diff --git a/.cargo/config.toml b/.cargo/config.toml
index c7356c5..bb28fe0 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -10,6 +10,3 @@ git-fetch-with-cli = true
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
-
-[target.i686-unknown-linux-gnu]
-linker = "i686-linux-gnu-gcc"
diff --git a/.gitea/workflows/main.yaml b/.gitea/workflows/main.yaml
index f201502..b9fac9e 100644
--- a/.gitea/workflows/main.yaml
+++ b/.gitea/workflows/main.yaml
@@ -64,7 +64,7 @@ jobs:
- name: Install Cross-Compilation Toolchains (apt)
run: |
sudo apt-get update
- sudo apt-get install -y gcc-aarch64-linux-gnu gcc-i686-linux-gnu
+ sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Ermittle Rust-Version für Rustup-Target-Cache-Key
run: echo "RUST_VERSION=$(rustc --version | awk '{print $2}')" >> "$GITHUB_ENV"
@@ -75,12 +75,11 @@ jobs:
with:
path: |
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/aarch64-unknown-linux-gnu
- ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/i686-unknown-linux-gnu
key: rustup-targets-${{ runner.os }}-${{ env.RUST_VERSION }}
- name: Add Rust Cross-Compilation Targets
if: steps.cache-rustup-targets.outputs.cache-hit != 'true'
- run: rustup target add aarch64-unknown-linux-gnu i686-unknown-linux-gnu
+ run: rustup target add aarch64-unknown-linux-gnu
- name: PATH um Cargo-bin-Verzeichnis ergänzen
run: |
@@ -111,7 +110,6 @@ jobs:
run: |
cargo build --release --target x86_64-unknown-linux-gnu
cargo build --release --target aarch64-unknown-linux-gnu
- cargo build --release --target i686-unknown-linux-gnu
- name: Determine Build Number
id: build_num
@@ -130,20 +128,17 @@ jobs:
run: |
cargo deb --target x86_64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
cargo deb --target aarch64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
- cargo deb --target i686-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
- name: Build Fedora / RPM Packages (.rpm)
run: |
mkdir -p target/generate-rpm
cargo generate-rpm --target x86_64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
cargo generate-rpm --target aarch64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
- cargo generate-rpm --target i686-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
- name: Build Arch Linux Packages (.pkg.tar.zst)
run: |
python3 scripts/package-arch.py --target x86_64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
python3 scripts/package-arch.py --target aarch64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
- python3 scripts/package-arch.py --target i686-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
- name: Publish Packages to Gitea Package Registry
env:
diff --git a/.gitea/workflows/renovate.yaml b/.gitea/workflows/renovate.yaml
index f9f6456..ccbdcb2 100644
--- a/.gitea/workflows/renovate.yaml
+++ b/.gitea/workflows/renovate.yaml
@@ -9,7 +9,7 @@ jobs:
renovate:
name: Dependency-Updates prüfen & Pull Requests erstellen
runs-on: ubuntu-latest
- container: ghcr.io/renovatebot/renovate:44.82.0
+ container: ghcr.io/renovatebot/renovate:44.83.0
steps:
- name: Renovate ausführen
run: renovate
diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml
index 6bf6d29..42cf728 100644
--- a/.gitea/workflows/testing.yaml
+++ b/.gitea/workflows/testing.yaml
@@ -64,7 +64,7 @@ jobs:
- name: Install Cross-Compilation Toolchains (apt)
run: |
sudo apt-get update
- sudo apt-get install -y gcc-aarch64-linux-gnu gcc-i686-linux-gnu
+ sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Ermittle Rust-Version für Rustup-Target-Cache-Key
run: echo "RUST_VERSION=$(rustc --version | awk '{print $2}')" >> "$GITHUB_ENV"
@@ -75,12 +75,11 @@ jobs:
with:
path: |
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/aarch64-unknown-linux-gnu
- ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/i686-unknown-linux-gnu
key: rustup-targets-${{ runner.os }}-${{ env.RUST_VERSION }}
- name: Add Rust Cross-Compilation Targets
if: steps.cache-rustup-targets.outputs.cache-hit != 'true'
- run: rustup target add aarch64-unknown-linux-gnu i686-unknown-linux-gnu
+ run: rustup target add aarch64-unknown-linux-gnu
- name: PATH um Cargo-bin-Verzeichnis ergänzen
run: |
@@ -111,7 +110,6 @@ jobs:
run: |
cargo build --release --target x86_64-unknown-linux-gnu
cargo build --release --target aarch64-unknown-linux-gnu
- cargo build --release --target i686-unknown-linux-gnu
- name: Determine Build Number
id: build_num
@@ -130,20 +128,17 @@ jobs:
run: |
cargo deb --target x86_64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
cargo deb --target aarch64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
- cargo deb --target i686-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
- name: Build Fedora / RPM Packages (.rpm)
run: |
mkdir -p target/generate-rpm
cargo generate-rpm --target x86_64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
cargo generate-rpm --target aarch64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
- cargo generate-rpm --target i686-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
- name: Build Arch Linux Packages (.pkg.tar.zst)
run: |
python3 scripts/package-arch.py --target x86_64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
python3 scripts/package-arch.py --target aarch64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
- python3 scripts/package-arch.py --target i686-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
- name: Publish Packages to Gitea Package Registry
env:
diff --git a/.idea/jsonSchemas.xml b/.idea/jsonSchemas.xml
index c3ebb13..35e53b8 100644
--- a/.idea/jsonSchemas.xml
+++ b/.idea/jsonSchemas.xml
@@ -32,6 +32,12 @@
-
+ -
+
+
+ -
+
+
diff --git a/.idea/rust-template.iml b/.idea/rust-template.iml
index cf84ae4..bbe0a70 100644
--- a/.idea/rust-template.iml
+++ b/.idea/rust-template.iml
@@ -3,6 +3,7 @@
+
diff --git a/AGENTS.md b/AGENTS.md
index 3b7ff31..111b50e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -6,17 +6,28 @@ Dieses Dokument dient als technischer Leitfaden und Kontextdokument für KI-Codi
## 1. Projektübersicht & Philosophie
-Dieses Repository ist ein **Rust-Projekt-Template** für Linux-Anwendungen und CLI-Tools mit Fokus auf:
-- Automatisierte Multi-Architektur-Kompilierung (`x86_64`, `aarch64`, `i686`).
-- Native Paketierung für Debian (`.deb`), Fedora/RHEL (`.rpm`) und Arch Linux (`.pkg.tar.zst`) sowie Docker-Container-Images.
+**mac2ip** ist ein CLI-Tool, das zuverlässig die aktuelle IP-Adresse zu einer gegebenen MAC-Adresse im lokalen Netzwerk findet, über einen 3-stufigen Algorithmus:
+
+1. **Cache** (`src/cache.rs`): globaler, systemweiter Cache auf Basis der `turso`-Crate (lokale Datei, Standard `/var/lib/mac2ip/cache.db`). Ein Treffer wird nur verwendet, wenn er nicht abgelaufen ist (TTL, `src/config.rs`) **und** die IP per Ping erreichbar ist.
+2. **`ip neigh`** (`src/network.rs`): moderner Ersatz für `arp`. Treffer nur bei Ping-Erreichbarkeit.
+3. **`nmap -sn`** (`src/network.rs`): ARP-/Ping-Scan der lokal angeschlossenen Subnetze. MAC-Adressen erscheinen in der nmap-Ausgabe nur mit Root-Rechten (Raw-Socket/libpcap), daher läuft dieser Schritt über `sudo nmap` (bzw. `nmap` direkt, falls schon root). Kein Treffer → Fehler ("nicht gefunden").
+
+Vor Schritt 3 prüft `src/trust.rs`, ob das aktuelle Netzwerk (identifiziert über die MAC-Adresse seines Default-Gateways) für einen nmap-Scan vertrauenswürdig ist (statische Liste in `AppConfig::trusted_networks`, zuvor per interaktiver Rückfrage/`--auto-trust-networks` im Cache bestätigtes Netzwerk). Ist das Netzwerk unbekannt, wird im interaktiven Modus nachgefragt; im `--json`-Modus (keine Rückfrage möglich) wird der Scan abgelehnt.
+
+Die Orchestrierung dieser drei Schritte liegt in `src/resolver.rs`; `src/main.rs` ist nur ein dünner Entry-Point (CLI-Parsing via `src/cli.rs`, Konfiguration via `src/config.rs` + `config-ctdra`, Logging via `src/log.rs` + `logger-ctdra`).
+
+Zur Laufzeit werden folgende System-Tools benötigt: `ip` (iproute2), `ping` (iputils), `nmap`, `sudo` (nur für Schritt 3, falls nicht schon root). Diese sind in den Paketierungs-Metadaten (`Cargo.toml`, siehe §3.2) als Abhängigkeiten hinterlegt.
+
+Das Projekt basiert auf einem generischen **Rust-Projekt-Template** für Linux-Anwendungen/CLI-Tools mit Fokus auf:
+- Automatisierte Multi-Architektur-Kompilierung (`x86_64`, `aarch64`; `i686` derzeit ausgesetzt, siehe §5.2.1).
+- Native Paketierung für Debian (`.deb`), Fedora/RHEL (`.rpm`) und Arch Linux (`.pkg.tar.zst`).
- Vollständig automatisierte CI/CD-Pipelines via Gitea Actions (kompatibel mit Forgejo / GitHub Actions).
- Automatisierte Sicherheits-Scans (Schwachstellen, Secrets) und Dependency-Updates.
### Standards & Tech-Stack
- **Sprache**: Rust (Edition 2024), Python 3 (für Hilfsskripte in `scripts/`).
- **Rust Toolchain**: Stable.
-- **Zielplattform**: Linux (GLIBC-basiert, Cross-Kompilierung für `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `i686-unknown-linux-gnu`).
-- **Container**: Docker-Images werden zusätzlich zu den nativen Paketen gebaut und in die Gitea Container Registry veröffentlicht.
+- **Zielplattform**: Linux (GLIBC-basiert, Cross-Kompilierung für `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`; `i686-unknown-linux-gnu` derzeit ausgesetzt, siehe §5.2.1).
- **Sicherheits-Tooling**: Trivy, OSV-Scanner, TruffleHog (Secret-Scanning), Renovate (Dependency-Updates), Qodana (statische Codeanalyse).
- **Lizenz**: GPL-3.0-or-later (sofern nicht im abgeleiteten Projekt anders definiert).
@@ -29,27 +40,41 @@ Dieses Repository ist ein **Rust-Projekt-Template** für Linux-Anwendungen und C
│ └── config.toml # Linker für Cross-Target-Kompilierung & Registry-Konfiguration
├── .gitea/
│ └── workflows/
-│ ├── main.yaml # CI/CD: Stabile Builds, Multi-Arch-Paketierung, Docker-Image, Release & Upload
-│ ├── testing.yaml # CI/CD: Preview-Builds, Docker-Image & Testing-Pakete
+│ ├── main.yaml # CI/CD: Stabile Builds, Multi-Arch-Paketierung, Release & Upload
+│ ├── testing.yaml # CI/CD: Preview-Builds & Testing-Pakete
│ ├── unit-tests.yaml # CI: Unit-Tests für Pull Requests gegen 'testing'
│ ├── security-scan.yaml # CI: Trivy & OSV-Scanner (Schwachstellen/Misconfig/Secrets)
│ ├── trufflehog-scan.yaml # CI: TruffleHog Secret-Scan (inkl. Git-Historie)
│ └── renovate.yaml # CI: Wöchentlicher Renovate-Lauf für Dependency-Updates
+├── packaging/
+│ ├── deb/postinst # Debian-Postinstall: legt /var/lib/mac2ip an
+│ └── arch/mac2ip.install # Arch-Install-Skriptlet: legt /var/lib/mac2ip an
├── scripts/
│ ├── get-build-number.py # Ermittelt automatisch die nächste Revisions-/Build-Nummer
-│ ├── package-arch.py # Erzeugt native Arch Linux .pkg.tar.zst Pakete
+│ ├── package-arch.py # Erzeugt native Arch Linux .pkg.tar.zst Pakete (inkl. optionalem Install-Skriptlet)
│ └── report-security-issue.py # Meldet Scan-Ergebnisse (Trivy/OSV/TruffleHog) als Gitea-Issue
├── src/
-│ └── main.rs # Einstiegspunkt der Anwendung
+│ ├── main.rs # Einstiegspunkt (CLI-Parsing, Wiring)
+│ ├── lib.rs # Modul-Wurzel (für Integrationstests)
+│ ├── cli.rs # clap-Kommandozeilen-Definition
+│ ├── config.rs # AppConfig + config-ctdra-Integration + CLI-Overlay
+│ ├── log.rs # JSON-Modus-bewusster logger-ctdra-Wrapper
+│ ├── mac.rs # MacAddress-Typ (Parsing/Kanonisierung)
+│ ├── cache.rs # Globaler Turso-Cache (MAC -> IP)
+│ ├── network.rs # ip neigh / ping / nmap: Exec- und Parse-Funktionen
+│ ├── trust.rs # Netzwerk-Vertrauensprüfung vor Schritt 3 (Gateway-MAC, Rückfrage, Cache)
+│ ├── resolver.rs # 3-Stufen-Algorithmus (Orchestrierung)
+│ └── output.rs # Human- und JSON-Ausgabe
+├── tests/ # Integrationstests (pure Parsing-/Logik-Funktionen, kein Netzwerk/root/nmap nötig)
├── Cargo.toml # Projekt-Manifest & Metadaten für deb, rpm und arch
├── qodana.yaml # Konfiguration für JetBrains Qodana (statische Analyse)
├── renovate.json # Renovate-Konfiguration (Gruppierung, Versions-Pins in Workflows)
├── LICENSE # Lizenztext
-├── README.md # Benutzerdokumentation & Setup-Checkliste
+├── README.md # Benutzerdokumentation
└── AGENTS.md # Dieses Agenten-Handbuch
```
-> **Hinweis:** `main.yaml`/`testing.yaml` bauen zusätzlich ein Docker-Image (`docker build .` mit `--build-arg TARGET_BIN=...`). Ein `Dockerfile` ist im Template noch **nicht** enthalten und muss von abgeleiteten Projekten ergänzt werden; der aktuell hartkodierte `TARGET_BIN`-Pfad (`.../release/mirror-package`) ist ein Platzhalter aus einem Referenzprojekt und muss beim Ableiten des Templates auf den tatsächlichen Binärnamen (`Cargo.toml` → `[package] name`) angepasst werden.
+> **Hinweis:** Dieses Projekt baut aktuell **kein** Docker-Image — keiner der Workflows unter `.gitea/workflows/` enthält einen Docker-Build-Schritt. Sollte das zukünftig ergänzt werden, muss ein `Dockerfile` hinzugefügt und der Binärname konsistent mit `[package] name` in `Cargo.toml` gehalten werden.
---
@@ -68,12 +93,13 @@ Bei Änderungen an Binärnamen, Abhängigkeiten oder Beschreibungen müssen die
2. `[package.metadata.generate-rpm]` (für `cargo-generate-rpm`):
- `requires`, `assets`.
3. `[package.metadata.arch]` (für `scripts/package-arch.py`):
- - `pkgrel`, `arch`, `depends`, `optdepends`.
+ - `pkgrel`, `arch`, `depends`, `optdepends`, optional `install_script` (Pfad zu einem pacman-Install-Skriptlet, siehe §3.3).
-Ändert sich der Binärname (`[package] name`), muss auch der `TARGET_BIN`-Build-Arg im Docker-Build-Step von `main.yaml`/`testing.yaml` sowie das (abzuleitende) `Dockerfile` angepasst werden.
+Alle drei Blöcke listen bei mac2ip zusätzlich `sudo`, `iproute2`/`iproute`, `iputils`/`iputils-ping` und `nmap` als Laufzeit-Abhängigkeiten (benötigt für den 3-Stufen-Algorithmus, siehe §1). `[package.metadata.deb].maintainer-scripts` sowie `[package.metadata.generate-rpm].post_install_script` legen beim Paket-Install `/var/lib/mac2ip` mit den nötigen Rechten an (siehe §1, "Caching & TTL" in README.md).
### 3.3 Skripte in `scripts/`
- **Generizität**: Die Skripte dürfen keine hardcodierten Anwendungsnamen, spezifischen Abhängigkeiten oder projektspezifischen URLs enthalten. Alle Werte müssen dynamisch aus `Cargo.toml` (via `cargo metadata` oder Dateiparsing) oder Umgebungsvariablen (`BUILD_NUMBER`, `GITEA_URL`, `REPO`, `TOKEN`) ermittelt werden.
+- **`package-arch.py`-Install-Skriptlet**: Liest optional `[package.metadata.arch].install_script` aus `cargo metadata` und bettet die referenzierte Datei pacman-konform als `.INSTALL` in das erzeugte `.pkg.tar.zst` ein — weiterhin vollständig metadatengetrieben, kein hartkodierter Anwendungsname im Skript selbst.
- **Python-Kompatibilität**: Verwende Standard-Python 3 ohne externe PyPI-Abhängigkeiten (nur Standardbibliothek: `json`, `subprocess`, `urllib`, `argparse`, `os`, `re`, `tempfile`, `tarfile` etc.).
- **`get-build-number.py`**: Ermittelt die nächste Build-/Revisions-Nummer nicht mehr rein lokal, sondern dynamisch über:
1. Gitea Releases API (Tag-/Asset-Namen),
@@ -126,6 +152,23 @@ Alle Workflows liegen unter `.gitea/workflows/` und nutzen gecachte Abhängigkei
- **`qodana.yaml`**: Konfiguration für JetBrains Qodana (statische Codeanalyse, Profil `qodana.starter`); wird über eine externe/IDE-seitige Qodana-CI-Integration ausgeführt, nicht über einen eigenen Gitea-Workflow in diesem Template.
- **`renovate.yaml`** (Trigger: wöchentlich montags 04:00 UTC, `workflow_dispatch`): Führt Renovate (Container-Image, gepinnte Version) gegen die Gitea-Plattform aus und erstellt Pull Requests für veraltete Abhängigkeiten gemäß `renovate.json`.
+### 5.2.1 Bekannte Sicherheits-Workarounds (temporär, mit Revert-Bedingung)
+
+Workarounds für Schwachstellen in transitiven Abhängigkeiten, die nicht direkt
+per Versions-Bump behoben werden können (z. B. weil ein Zwischenpaket eine zu
+enge Versionsanforderung pinnt). Vor jedem Renovate-Merge/Dependency-Update
+prüfen, ob die Revert-Bedingung inzwischen erfüllt ist.
+
+| Advisory | Betroffenes Paket | Workaround | Wo dokumentiert | Revert-Bedingung |
+| :--- | :--- | :--- | :--- | :--- |
+| [RUSTSEC-2026-0253](https://rustsec.org/advisories/RUSTSEC-2026-0253.html) (Use-after-free in `LruCache::pop()`) | `lru` < 0.18.2, transitiv über `turso -> turso_core -> tantivy` | `fts`-Default-Feature von `turso` deaktiviert (`Cargo.toml`) — mac2ip nutzt keine Volltextsuche, dadurch entfällt `tantivy`/`lru` komplett aus dem Dependency-Baum | Kommentar direkt über der `turso`-Zeile in `Cargo.toml` | Sobald `cargo update -p lru --precise 0.18.2` erfolgreich ist (d. h. eine stabile `turso`-Version eine `tantivy`-Version mit `lru >= 0.18.2` zulässt): `turso`-Zeile zurück auf `turso = "0.7"` (oder aktueller) ändern, `cargo update -p turso`, und `cargo tree -i lru` zur Kontrolle. |
+
+### 5.2.2 Bekannte Build-Einschränkungen (temporär, mit Revert-Bedingung)
+
+| Einschränkung | Ursache | Workaround | Wo dokumentiert | Revert-Bedingung |
+| :--- | :--- | :--- | :--- | :--- |
+| Kein `i686-unknown-linux-gnu`-Build/-Paket mehr | `turso_sdk_kit` (Abhängigkeit von `turso`) fordert von `turso_core` unconditional das Feature `io_uring` an (nicht abschaltbar wie `fts`). Die `io-uring`-Crate (aktuell 0.7.15, neueste Version) liefert vorgefertigte FFI-Bindings nur für `x86_64`, `aarch64`, `riscv64`, `powerpc64`, `loongarch64` - **nicht** für `i686`. `--cfg=io_uring_skip_arch_check` würde auf i686 stillschweigend die x86_64-Bindings verwenden (falsche Struct-Layouts/Pointer-Breite -> potenzielle Speicherkorruption bei io_uring-Syscalls) und wurde deshalb bewusst NICHT verwendet. | `i686-unknown-linux-gnu` komplett aus den Build-Targets entfernt: `.cargo/config.toml` (Linker-Eintrag), `.gitea/workflows/main.yaml` + `testing.yaml` (Toolchain-Install, `rustup target add`, `cargo build`/`cargo deb`/`cargo generate-rpm`/`package-arch.py`-Aufrufe), `README.md`/`AGENTS.md` (Zielarchitektur-Angaben) | Diese Tabelle; `git log` nach "i686" für die genauen Commits | Sobald eine neue `io-uring`-Version vorgefertigte `i686`-Bindings mitbringt (`cargo info io-uring` prüfen) ODER eine neue `turso`-Version `io_uring` optional/abschaltbar macht: i686 wieder in allen oben genannten Dateien ergänzen und mit einem echten i686-Cross-Build verifizieren, bevor gemergt wird. |
+
### 5.3 Renovate-Konfiguration (`renovate.json`)
- Basis-Branch für PRs: `dev`.
- Gruppiert Updates nach `Gitea Actions`, `Cargo Dependencies` und `Docker-Images`.
diff --git a/Cargo.lock b/Cargo.lock
index ad94a61..d403a21 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,5 +3,2846 @@
version = 4
[[package]]
-name = "rust-template"
-version = "0.0.0"
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common",
+ "generic-array",
+]
+
+[[package]]
+name = "aegis"
+version = "0.9.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58541132f980da31e9aa99f7bdee69bc84bf1e168b9b91ef2dbe8abb7b4ce5dd"
+dependencies = [
+ "cc",
+ "softaes",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures 0.2.17",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
+dependencies = [
+ "aead",
+ "aes",
+ "cipher",
+ "ctr",
+ "ghash",
+ "subtle",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c880a97d28a3681c0267bd29cff89621202715b065127cd445fa0f0fe0aa2880"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
+dependencies = [
+ "libc 0.2.189",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "antithesis_sdk"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08410fcac93669a476c006cd6c4512ac1e2b30fd117231a5d55d8a2c76599b82"
+dependencies = [
+ "libc 0.2.189",
+ "libloading",
+ "linkme",
+ "once_cell",
+ "rand 0.8.8",
+ "rustc_version_runtime",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+
+[[package]]
+name = "arc-swap"
+version = "1.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "aristo"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcdeefa800110050103e2459a2f8dbdbd560ad046e30f1f87e24ce19c2cb8bde"
+dependencies = [
+ "aristo-macros",
+]
+
+[[package]]
+name = "aristo-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d64a66d21a80182b35b1741997a6d2456911f54b7eb1918aa4e2382fc205268d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "assoc"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfdc70193dadb9d7287fa4b633f15f90c876915b31f6af17da307fc59c9859a8"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bigdecimal"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695"
+dependencies = [
+ "autocfg",
+ "libm",
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.69.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "itertools 0.12.1",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash 1.1.0",
+ "shlex 1.3.0",
+ "syn 2.0.119",
+ "which",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
+
+[[package]]
+name = "bitvec"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "branches"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7436b21fd6195415058eb41c9090e156b556bc7e0377bb57c5c026a421521525"
+dependencies = [
+ "rustc_version",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
+dependencies = [
+ "bytemuck_derive",
+]
+
+[[package]]
+name = "bytemuck_derive"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
+name = "cc"
+version = "1.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4"
+dependencies = [
+ "find-msvc-tools",
+ "shlex 2.0.1",
+]
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "cfg_block"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18758054972164c3264f7c8386f5fc6da6114cb46b619fd365d4e3b2dc3ae487"
+
+[[package]]
+name = "chacha20"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.1",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "clang-sys"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
+dependencies = [
+ "glob",
+ "libc 0.2.189",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "config-ctdra"
+version = "1.0.6"
+source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
+checksum = "83eb23d473fce79e8234ad66baf210289b5c9e4c4a587273b4df7deceb73f416"
+dependencies = [
+ "confy",
+ "program-ctdra",
+ "serde",
+ "sudo-ctdra",
+]
+
+[[package]]
+name = "confy"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8807c397789cbe02bbdb1a27ea5f345584132808697b2a3f957c829829ee4814"
+dependencies = [
+ "etcetera",
+ "lazy_static",
+ "serde",
+ "thiserror",
+ "toml",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc 0.2.189",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
+dependencies = [
+ "libc 0.2.189",
+]
+
+[[package]]
+name = "crc32c"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47"
+dependencies = [
+ "rustc_version",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+ "typenum",
+]
+
+[[package]]
+name = "ctr"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+
+[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "either"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
+
+[[package]]
+name = "env_filter"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6"
+dependencies = [
+ "env_filter",
+ "log",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc 0.2.189",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "etcetera"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6"
+dependencies = [
+ "cfg-if",
+ "home",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+[[package]]
+name = "fastbloom"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4"
+dependencies = [
+ "getrandom 0.3.4",
+ "libm",
+ "siphasher",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "genawaiter"
+version = "0.99.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0"
+dependencies = [
+ "genawaiter-macro",
+]
+
+[[package]]
+name = "genawaiter-macro"
+version = "0.99.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc"
+
+[[package]]
+name = "generator"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "libc 0.2.189",
+ "log",
+ "rustversion",
+ "windows-link",
+ "windows-result",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc 0.2.189",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc 0.2.189",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "libc 0.2.189",
+ "r-efi 6.0.0",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "ghash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "home"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collator"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08984ed58ac439ebf3e13d2cf26b0c46a60afcd21721c1d14087c0b240344dda"
+dependencies = [
+ "icu_collator_data",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_locale_fallback",
+ "icu_normalizer",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "utf16_iter",
+ "utf8_iter",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_collator_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d7e54efdddeb1208c08dd5d32b53a879ac00d2d3d051b2255fd26821d16368"
+
+[[package]]
+name = "icu_collections"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "785f595c61ef57169a467eeed7b4b6936a66f6cacbb116a96ee86da983251bf4"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_locale_data",
+ "icu_locale_fallback",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "serde",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d37d91460e362a5cf58907cd7ce871411775ee6294e49a5cc8e6cec16dfa75ea"
+
+[[package]]
+name = "icu_locale_fallback"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9"
+dependencies = [
+ "icu_locale_core",
+ "icu_locale_fallback_data",
+ "icu_provider",
+ "potential_utf",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_fallback_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8"
+
+[[package]]
+name = "icu_normalizer"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "utf16_iter",
+ "utf8_iter",
+ "write16",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
+
+[[package]]
+name = "icu_properties"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
+
+[[package]]
+name = "icu_provider"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "serde",
+ "stable_deref_trait",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "intrusive-collections"
+version = "0.9.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86"
+dependencies = [
+ "memoffset",
+]
+
+[[package]]
+name = "io-uring"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3bd0ecfbb87805f538bb7b32e5239ca0763890c623e349860ecba69469f2bb"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "libc 0.2.189",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "libc"
+version = "1.0.0-alpha.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d0f24f33af482526a4e3f9b47f0abb2c6377a1713c8aa4a8106994689a4cfa5"
+
+[[package]]
+name = "libloading"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link",
+]
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "libmimalloc-sys"
+version = "0.1.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "linkme"
+version = "0.3.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5"
+dependencies = [
+ "linkme-impl",
+]
+
+[[package]]
+name = "linkme-impl"
+version = "0.3.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+
+[[package]]
+name = "logger-ctdra"
+version = "1.0.5"
+source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
+checksum = "ce612b7d943b77060fa36eab0c85782e650c4cf023e9d560bef791951ed92cad"
+dependencies = [
+ "program-ctdra",
+ "time",
+]
+
+[[package]]
+name = "loom"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
+dependencies = [
+ "cfg-if",
+ "generator",
+ "scoped-tls",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "mac2ip"
+version = "1.0.0"
+dependencies = [
+ "clap",
+ "config-ctdra",
+ "logger-ctdra",
+ "serde",
+ "serde_json",
+ "sudo-ctdra",
+ "thiserror",
+ "tokio",
+ "turso",
+]
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "miette"
+version = "7.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
+dependencies = [
+ "cfg-if",
+ "miette-derive",
+ "unicode-width",
+]
+
+[[package]]
+name = "miette-derive"
+version = "7.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "mimalloc"
+version = "0.1.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862"
+dependencies = [
+ "libmimalloc-sys",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "mio"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
+dependencies = [
+ "libc 0.2.189",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num-integer"
+version = "0.1.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_threads"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
+dependencies = [
+ "libc 0.2.189",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
+[[package]]
+name = "owo-colors"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
+
+[[package]]
+name = "pack1"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3b7bb0ecf2e447b1f20ee94ee79ef6eed1e9d4b3c36ce1903b9dea3bf205523"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc 0.2.189",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "pastey"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix 1.1.4",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "polyval"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
+dependencies = [
+ "serde_core",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "program-ctdra"
+version = "1.0.1"
+source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
+checksum = "528d5771916f74bdffb77ad60ea00c0ae4bd85f9a84e920cd65730a082133d67"
+
+[[package]]
+name = "prost"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
+dependencies = [
+ "anyhow",
+ "itertools 0.14.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
+dependencies = [
+ "libc 0.2.189",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e"
+dependencies = [
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rapidhash"
+version = "4.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "roaring"
+version = "0.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18bd8a37d17a58532776dcdf6041ce64929adca78e8489d5cacbafe99229d3e1"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustc_version_runtime"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d"
+dependencies = [
+ "rustc_version",
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc 0.2.189",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc 0.2.189",
+ "linux-raw-sys 0.12.1",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.151"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "sha1_smol"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "shuttle"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ab17edba38d63047f46780cf7360acf7467fec2c048928689a5c1dd1c2b4e31"
+dependencies = [
+ "assoc",
+ "bitvec",
+ "cfg-if",
+ "generator",
+ "hex",
+ "owo-colors",
+ "rand 0.8.8",
+ "rand_core 0.6.4",
+ "rand_pcg",
+ "scoped-tls",
+ "smallvec",
+ "tracing",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc 0.2.189",
+]
+
+[[package]]
+name = "simsimd"
+version = "6.5.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4fb3bc3cdce07a7d7d4caa4c54f8aa967f6be41690482b54b24100a2253fa70"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "siphasher"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
+
+[[package]]
+name = "softaes"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45e14297decde697ddf377c25752aead0927d5cfc89c2684d2af96901a4ceeea"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "strum"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "sudo-ctdra"
+version = "1.0.1"
+source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
+checksum = "20e576be60eb2050d475d0fbe46f0d09462ba9985ab55c7833e0c2e6d116d341"
+dependencies = [
+ "libc 1.0.0-alpha.4",
+]
+
+[[package]]
+name = "symlink"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.3",
+ "once_cell",
+ "rustix 1.1.4",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "time"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+dependencies = [
+ "deranged",
+ "libc 0.2.189",
+ "num-conv",
+ "num_threads",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
+dependencies = [
+ "displaydoc",
+ "serde_core",
+ "zerovec",
+]
+
+[[package]]
+name = "tokio"
+version = "1.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
+dependencies = [
+ "bytes",
+ "libc 0.2.189",
+ "mio",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.15",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.3+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
+dependencies = [
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-appender"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
+dependencies = [
+ "crossbeam-channel",
+ "symlink",
+ "thiserror",
+ "time",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "turso"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9491d7a80312c5abe66a4409e4dce02065503a235453c94b9e4133877e39ffc"
+dependencies = [
+ "mimalloc",
+ "thiserror",
+ "tracing",
+ "tracing-subscriber",
+ "turso_core",
+ "turso_sdk_kit",
+ "turso_sync_sdk_kit",
+]
+
+[[package]]
+name = "turso_core"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a833cc3bf8d4e6c101c504fa470f8ab4270c2202ff2591b61b2e373b4f20d9b"
+dependencies = [
+ "aegis",
+ "aes",
+ "aes-gcm",
+ "allocator-api2",
+ "antithesis_sdk",
+ "arc-swap",
+ "aristo",
+ "bigdecimal",
+ "bitflags",
+ "branches",
+ "bumpalo",
+ "bytemuck",
+ "cfg_aliases",
+ "cfg_block",
+ "chrono",
+ "crc32c",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "either",
+ "fallible-iterator",
+ "fastbloom",
+ "hex",
+ "icu_collator",
+ "icu_locale",
+ "intrusive-collections",
+ "io-uring",
+ "libc 0.2.189",
+ "libloading",
+ "libm",
+ "loom",
+ "miette",
+ "num-bigint",
+ "num-traits",
+ "pack1",
+ "parking_lot",
+ "pastey",
+ "polling",
+ "rand 0.9.5",
+ "rapidhash",
+ "regex",
+ "regex-syntax",
+ "roaring",
+ "rustc-hash 2.1.3",
+ "rustix 1.1.4",
+ "ryu",
+ "serde_json",
+ "shuttle",
+ "simsimd",
+ "smallvec",
+ "strum",
+ "strum_macros",
+ "tempfile",
+ "thiserror",
+ "tracing",
+ "tracing-subscriber",
+ "turso_ext",
+ "turso_macros",
+ "turso_parser",
+ "twox-hash",
+ "uncased",
+ "uuid",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "turso_ext"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7452fe676450b6840e9eb80e512d14293265221154eba66aba3845dfc0d659f"
+dependencies = [
+ "chrono",
+ "getrandom 0.4.3",
+ "turso_macros",
+]
+
+[[package]]
+name = "turso_macros"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2825eaa6b7b893693636947f988e252c1196393fa54d4b85637c70d616d92fa"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "turso_parser"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0e58d029f02e442df4be0b5036ce520b49c553505d9d52ef5a98fc4056e95b7"
+dependencies = [
+ "bitflags",
+ "memchr",
+ "miette",
+ "strum",
+ "strum_macros",
+ "thiserror",
+ "turso_macros",
+]
+
+[[package]]
+name = "turso_sdk_kit"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18c1dc1c0304348c39b97bc6b27cdcb1d7292454ebd0de0f30b5ee3a4c61f9bb"
+dependencies = [
+ "bindgen",
+ "env_logger",
+ "parking_lot",
+ "tracing",
+ "tracing-appender",
+ "tracing-subscriber",
+ "turso_core",
+ "turso_ext",
+ "turso_sdk_kit_macros",
+]
+
+[[package]]
+name = "turso_sdk_kit_macros"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4708b5fe678b8730c85964e3d378f75e18c23c4409971360b2209f4e53e4956"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "turso_sync_engine"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce8f03e430240d590d209ac874db52773b7382c627cd604a685affde77622b09"
+dependencies = [
+ "base64",
+ "bytes",
+ "crc32c",
+ "genawaiter",
+ "http",
+ "libc 0.2.189",
+ "prost",
+ "roaring",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tracing",
+ "turso_core",
+ "turso_parser",
+ "uuid",
+]
+
+[[package]]
+name = "turso_sync_sdk_kit"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d48cb47d056c3ec567745761fa6f832358ca30ef9eb0435fdcfb77c558e70089"
+dependencies = [
+ "bindgen",
+ "env_logger",
+ "genawaiter",
+ "parking_lot",
+ "tracing",
+ "tracing-appender",
+ "tracing-subscriber",
+ "turso_core",
+ "turso_sdk_kit",
+ "turso_sdk_kit_macros",
+ "turso_sync_engine",
+]
+
+[[package]]
+name = "twox-hash"
+version = "2.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a"
+dependencies = [
+ "rand 0.10.2",
+]
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "uncased"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "utf16_iter"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
+dependencies = [
+ "getrandom 0.4.3",
+ "js-sys",
+ "sha1_smol",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.128"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.128"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.128"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.128"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "which"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix 0.38.44",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+
+[[package]]
+name = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "write16"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
+
+[[package]]
+name = "writeable"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
+dependencies = [
+ "serde",
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/Cargo.toml b/Cargo.toml
index 0cb0754..620b025 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,15 +1,36 @@
[package]
-name = "rust-template" # TODO: Projektnamen setzen
-version = "0.0.0" # TODO: Initiale Version setzen (z. B. 0.1.0)
+name = "mac2ip"
+version = "1.0.0"
edition = "2024"
-authors = ['DragonSlayer_14'] # TODO: Autor(en) anpassen
+authors = ['DragonSlayer_14']
readme = "README.md"
-license = "GPL-3.0-or-later" # TODO: Lizenz prüfen / anpassen
-repository = "https://gitea.creative-dragonslayer.de/Linuxapps/rust-template" # TODO: Repository-URL anpassen
-description = "" # TODO: Kurze Projektbeschreibung setzen
+license = "GPL-3.0-or-later"
+repository = "https://gitea.creative-dragonslayer.de/Linuxapps/Mac2Ip"
+description = "Findet zuverlässig die aktuelle IP-Adresse zu einer MAC-Adresse im lokalen Netzwerk (Cache, ip neigh, nmap)"
[dependencies]
-# TODO: Projektabhängigkeiten hier hinzufügen
+config-ctdra = { version = "1.0.6", registry = "gitea" }
+logger-ctdra = { version = "1.0.5", registry = "gitea" }
+sudo-ctdra = { version = "1.0.1", registry = "gitea" }
+tokio = { version = "1", features = ["macros", "rt-multi-thread", "process", "time"] }
+# WORKAROUND für RUSTSEC-2026-0253 (Use-after-free in lru < 0.18.2): das
+# Default-Feature "fts" zieht tantivy 0.26.2 -> lru "^0.16.3" (verwundbar) ein,
+# obwohl mac2ip keine Volltextsuche nutzt. "fts" bleibt deaktiviert, bis eine
+# stabile turso-Version tantivy>=eine lru>=0.18.2 zulassende Version pinnt.
+#
+# Revert, sobald behoben (z. B. Renovate-PR/manuelle Prüfung):
+# 1. `cargo update -p lru --precise 0.18.2` probieren - schlägt fehl, solange
+# das o. g. Problem besteht (siehe Fehlermeldung mit dem Pfad turso ->
+# turso_core -> tantivy -> lru "^0.16.3").
+# 2. Sobald das gelingt (oder eine neue stabile turso-Version das Problem
+# upstream behebt): diese Zeile zurück zu `turso = "0.7"` (oder aktueller)
+# ändern und `cargo update -p turso` laufen lassen.
+# 3. `cargo tree -i lru` zur Kontrolle - keine Version < 0.18.2 mehr vorhanden.
+turso = { version = "0.7", default-features = false, features = ["mimalloc"] }
+clap = { version = "4", features = ["derive", "env"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+thiserror = "2"
[profile.release]
debug = "none"
@@ -17,28 +38,40 @@ debug = "none"
# --- Paketierungs-Metadaten für Linux-Distributionen ---
[package.metadata.deb]
-name = "" # TODO: Optional (Standard: package.name)
-maintainer = "" # TODO: Maintainer setzen (z. B. "Max Mustermann ")
-copyright = "" # TODO: Copyright-Hinweis setzen (z. B. "2026 Max Mustermann")
-section = "utils" # TODO: Debian-Sektion anpassen (z. B. utils, net, admin)
-priority = "optional" # TODO: Debian-Priorität anpassen (z. B. optional)
-depends = "$auto" # TODO: Abhängigkeiten definieren ($auto ermittelt via ldd automatisch)
+name = "mac2ip"
+maintainer = "DragonSlayer_14"
+copyright = "2026 DragonSlayer_14"
+section = "net"
+priority = "optional"
+depends = "$auto, sudo, iproute2, iputils-ping, nmap"
+maintainer-scripts = "packaging/deb"
extended-description = """\
-""" # TODO: Ausführliche Paketbeschreibung setzen
+mac2ip ermittelt zuverlässig die aktuelle IP-Adresse zu einer gegebenen MAC-Adresse
+im lokalen Netzwerk. Dazu wird zunächst ein globaler, systemweiter Cache geprüft,
+danach die 'ip neigh'-Nachbartabelle (moderner Ersatz für 'arp'), und als letzter
+Schritt ein nmap-Ping-/ARP-Scan der lokal angeschlossenen Subnetze durchgeführt.\
+"""
assets = [
- # TODO: Bei Bedarf zusätzliche Dateien / Assets definieren, z. B.:
- # ["target/release/rust-template", "usr/bin/", "755"],
+ ["target/release/mac2ip", "usr/bin/mac2ip", "755"],
+ ["README.md", "usr/share/doc/mac2ip/README.md", "644"],
+ ["LICENSE", "usr/share/doc/mac2ip/copyright", "644"],
]
[package.metadata.generate-rpm]
assets = [
- # TODO: Bei Bedarf zusätzliche Dateien / Assets definieren, z. B.:
- # { source = "target/release/rust-template", dest = "/usr/bin/rust-template", mode = "755" },
+ { source = "target/release/mac2ip", dest = "/usr/bin/mac2ip", mode = "755" },
+ { source = "README.md", dest = "/usr/share/doc/mac2ip/README.md", mode = "644", doc = true },
+ { source = "LICENSE", dest = "/usr/share/licenses/mac2ip/LICENSE", mode = "644", license = true },
]
-requires = { } # TODO: RPM-Abhängigkeiten definieren, z. B. { "glibc" = "*" }
+requires = { "sudo" = "*", "iproute" = "*", "iputils" = "*", "nmap" = "*" }
+post_install_script = """
+mkdir -p /var/lib/mac2ip
+chmod 0777 /var/lib/mac2ip
+"""
[package.metadata.arch]
-pkgrel = "1" # TODO: Standard Release-Nummer (wird in CI durch get-build-number.py dynamisch überschrieben)
-arch = "x86_64" # TODO: Zielarchitektur (z. B. x86_64, aarch64, i686)
-depends = ["gcc-libs", "glibc"] # TODO: Arch Linux Paketabhängigkeiten definieren
-optdepends = [] # TODO: Optionale Abhängigkeiten definieren (z. B. ["curl: für Web-Downloads"])
+pkgrel = "1"
+arch = "x86_64"
+depends = ["gcc-libs", "glibc", "sudo", "iproute2", "iputils", "nmap"]
+optdepends = []
+install_script = "packaging/arch/mac2ip.install"
diff --git a/README.md b/README.md
index 56e1fe8..1c263f4 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,134 @@
-# rust-template
+# mac2ip
-Ein modernes Template-Projekt für Rust-basierte Linux-Anwendungen und Kommandozeilen-Tools (CLI).
+[](https://gitea.creative-dragonslayer.de/Linuxapps/Mac2Ip/actions?workflow=main.yaml)
+[](https://gitea.creative-dragonslayer.de/Linuxapps/Mac2Ip/actions?workflow=testing.yaml)
-Dieses Template bietet eine vorkonfigurierte Umgebung für modernes Rust (Edition 2024), automatisierte Multi-Architektur-Kompilierung und native Paketierung für die gängigsten Linux-Distributionen (Debian/Ubuntu, Fedora/RHEL, Arch Linux) sowie vollständige CI/CD-Pipelines für Gitea Actions (kompatibel mit Forgejo und GitHub Actions).
+Ein Kommandozeilen-Tool (CLI), das zuverlässig die aktuelle IP-Adresse zu einer gegebenen MAC-Adresse im lokalen Netzwerk findet.
+
+Dazu wird ein 3-stufiger Algorithmus verwendet:
+
+1. **Cache**: Ein globaler, systemweiter Cache (für alle Nutzer dieses Rechners) wird geprüft. Ein Treffer wird nur verwendet, wenn er noch nicht abgelaufen ist (TTL) **und** die IP per Ping erreichbar ist.
+2. **`ip neigh`**: Die Linux-Nachbartabelle (`ip neigh show`, der moderne Ersatz für den veralteten `arp`-Befehl) wird nach der MAC-Adresse durchsucht. Ein Treffer wird nur verwendet, wenn die IP per Ping erreichbar ist.
+3. **`nmap`**: Als letzter Schritt wird ein Ping-/ARP-Scan (`nmap -sn`) der lokal angeschlossenen Subnetze durchgeführt. Da MAC-Adressen in der nmap-Ausgabe nur mit Root-Rechten sichtbar sind (ARP-Scans benötigen Raw-Socket-/libpcap-Zugriff), läuft dieser Schritt über `sudo nmap` (bzw. direkt `nmap`, falls das Programm bereits als root läuft). Da ein Subnetz-Scan auf fremden Netzwerken heikel sein kann, wird dieser Schritt nur in einem als vertrauenswürdig eingestuften Netzwerk ausgeführt (siehe [Netzwerk-Vertrauen](#netzwerk-vertrauen)). Wird auch hier keine erreichbare IP gefunden, bricht das Programm mit einem Fehler ("nicht gefunden") ab.
---
## Features
-- **Rust Edition 2024**: Moderner Rust-Sprachstandard.
-- **Multi-Architektur-Kompilierung**:
- - `x86_64-unknown-linux-gnu` (64-Bit x86)
- - `aarch64-unknown-linux-gnu` (64-Bit ARM / ARM64)
- - `i686-unknown-linux-gnu` (32-Bit x86)
-- **Linux-Paketierung out-of-the-box**:
- - **Debian / Ubuntu** (`.deb` via `cargo-deb`)
- - **Fedora / RHEL / openSUSE** (`.rpm` via `cargo-generate-rpm`)
- - **Arch Linux** (`.pkg.tar.zst` via mitgeliefertem `scripts/package-arch.py`)
-- **Automatisierte CI/CD-Pipelines**:
- - `main`-Branch: Erstellt stabile Builds, ermittelt dynamisch Build-Nummern, paketiert für alle Architekturen, lädt Pakete in die Gitea Package Registry und erstellt Gitea Releases mit Dateianhängen.
- - `testing`-Branch: Erstellt Preview-Builds und Pre-Releases mit Testing-Paketen.
-- **Automatisierte Versions- & Build-Nummern**:
- - `scripts/get-build-number.py` fragt Gitea Releases, die Package Registry sowie lokale Artefakte ab, um Revisions-/Release-Nummern (z. B. `1.0.0-1`, `1.0.0-2`) automatisch zu erhöhen.
-- **Vorkonfigurierte Cargo-Einstellungen**:
- - `.cargo/config.toml` mit vorkonfigurierten Cross-Compilation-Linkern und Unterstützung für private/öffentliche Cargo Registries.
+- **Zuverlässige MAC → IP-Auflösung** über Cache, `ip neigh` und `nmap`, jeweils mit Erreichbarkeitsprüfung per Ping.
+- **Globaler, systemweiter Cache** (via [`turso`](https://turso.tech), lokal-dateibasiert) mit konfigurierbarer TTL.
+- **Maschinenlesbare Ausgabe** über `--json` (unattended-Modus) — unterdrückt dabei alle sonstigen Log-Ausgaben.
+- **Vollständig über die Kommandozeile konfigurierbar**, mit Overlay-Kette CLI > Umgebungsvariable > Konfigurationsdatei > Standardwert.
+- **Funktioniert mit und ohne `sudo`** — Root-Rechte werden nur für den nmap-Schritt benötigt.
+- **Rust Edition 2024**, Multi-Architektur-Kompilierung (`x86_64`, `aarch64`), native Linux-Paketierung (`.deb`, `.rpm`, `.pkg.tar.zst`) und automatisierte CI/CD-Pipelines via Gitea Actions.
+
+---
+
+## CLI-Nutzung
+
+```bash
+mac2ip [OPTIONEN]
+```
+
+Beispiel:
+
+```bash
+$ mac2ip aa:bb:cc:dd:ee:ff
+aa:bb:cc:dd:ee:ff -> 192.168.1.42 (Quelle: cache)
+```
+
+Maschinenlesbare Ausgabe (unattended-Modus):
+
+```bash
+$ mac2ip aa:bb:cc:dd:ee:ff --json
+{"status":"ok","mac":"aa:bb:cc:dd:ee:ff","ip":"192.168.1.42","source":"cache"}
+```
+
+Bei Fehlern (`status: "error"`) ist `source` nicht enthalten, dafür ein `error`-Feld mit einer Beschreibung; der Exit-Code ist in beiden Fällen ungleich 0 (`1`) bei Fehlschlag.
+
+### Optionen
+
+| Option | Umgebungsvariable | Standard | Beschreibung |
+| :--- | :--- | :--- | :--- |
+| `--json` | - | aus | Gibt das Ergebnis als einzeiliges JSON-Objekt aus; unterdrückt alle sonstigen Log-Ausgaben. |
+| `--config ` | - | siehe unten | Benutzerdefinierter Pfad zur Konfigurationsdatei. |
+| `--log-level ` | `MAC2IP_LOG_LEVEL` | `info` | Logging-Level: `error`, `warn`, `info`, `debug`. |
+| `--cache-ttl-seconds ` | `MAC2IP_CACHE_TTL_SECONDS` | `1800` | Wie lange ein Cache-Eintrag als gültig angesehen wird (zusätzlich zur Ping-Prüfung). |
+| `--cache-db-path ` | `MAC2IP_CACHE_DB_PATH` | `/var/lib/mac2ip/cache.db` | Pfad zur globalen Cache-Datenbankdatei. |
+| `--nmap-timeout-seconds ` | `MAC2IP_NMAP_TIMEOUT_SECONDS` | `120` | Timeout für einen einzelnen nmap-Subnetz-Scan. |
+| `--networks ` | `MAC2IP_NETWORKS` | Auto-Erkennung | Kommagetrennte Liste von Subnetzen für den nmap-Scan; überschreibt die automatische Erkennung über `ip route`. |
+| `--trusted-networks ` | `MAC2IP_TRUSTED_NETWORKS` | leer | Kommagetrennte Liste von Gateway-MAC-Adressen, deren Netzwerke ohne Rückfrage für nmap-Scans (Schritt 3) vertraut werden; überschreibt die Konfigurationsdatei vollständig. |
+| `--auto-trust-networks` | - | aus | Beantwortet die "Netzwerk vertrauen?"-Rückfrage vor Schritt 3 automatisch mit Ja (und merkt sich das Netzwerk dauerhaft im Cache), statt interaktiv nachzufragen bzw. im `--json`-Modus abzulehnen. |
+
+Die Überschreibungs-Reihenfolge ist immer: **CLI-Flag > Umgebungsvariable > Konfigurationsdatei > Standardwert.**
+
+---
+
+## Konfigurationsdatei
+
+Die Konfiguration wird über [`config-ctdra`](https://gitea.creative-dragonslayer.de/Rust-Crates) verwaltet:
+
+- Als **root** (bzw. beim nmap-Schritt via `sudo`): `/etc/mac2ip/config.toml`
+- Als **normaler Nutzer**: `~/.config/mac2ip/config.toml`
+- Oder explizit über `--config `
+
+Beispiel:
+
+```toml
+cache_ttl_seconds = 1800
+cache_db_path = "/var/lib/mac2ip/cache.db"
+log_level = "info"
+nmap_timeout_seconds = 120
+networks = []
+trusted_networks = []
+```
+
+Alle Felder sind optional (fehlende Felder verwenden den Standardwert) und können, wie oben beschrieben, zusätzlich per CLI-Flag oder Umgebungsvariable überschrieben werden.
+
+---
+
+## Caching & TTL
+
+Der Cache liegt standardmäßig unter `/var/lib/mac2ip/cache.db` — **global für alle Nutzer des Rechners**, nicht pro Benutzerkonto. Verzeichnis und Datei sind bewusst world-writable (`0777`/`0666`), damit auch unprivilegierte Nutzer den Cache lesen und schreiben können, ohne dass mac2ip dafür Root-Rechte bräuchte.
+
+Ein Cache-Treffer wird nur verwendet, wenn:
+1. der Eintrag noch nicht älter als `cache_ttl_seconds` ist (Standard: 30 Minuten), **und**
+2. die gespeicherte IP-Adresse aktuell per Ping erreichbar ist.
+
+Die Ping-Prüfung ist die primäre Absicherung gegen veraltete Zuordnungen; die TTL ist eine zusätzliche Absicherung für den Fall, dass eine alte IP-Adresse inzwischen an ein anderes, ebenfalls erreichbares Gerät vergeben wurde (z. B. nach einem DHCP-Lease-Wechsel).
+
+Kann das Cache-Verzeichnis beim Programmstart nicht angelegt/beschrieben werden (z. B. bei einem Entwicklungslauf ohne vorherige Paketinstallation), wird der Cache für diesen Lauf einfach deaktiviert (eine Warnung wird geloggt, im `--json`-Modus unterdrückt) — mac2ip führt den Lookup dann ohne Cache über `ip neigh`/`nmap` durch. Bei einer Installation über `.deb`/`.rpm`/`.pkg.tar.zst` wird das Verzeichnis automatisch mit den richtigen Rechten angelegt (siehe Paketierungs-Postinstall-Skripte unter `packaging/`).
+
+---
+
+## Netzwerk-Vertrauen
+
+Ein `nmap`-Subnetz-Scan (Schritt 3) ist auf Netzwerken, die man nicht selbst administriert (Firmen-/Gast-WLAN, Kundenstandort etc.), potenziell heikel — er kann IDS-Alarme auslösen oder gegen eine Nutzungsordnung verstoßen. mac2ip führt Schritt 3 deshalb nur in einem als vertrauenswürdig eingestuften Netzwerk aus.
+
+Das aktuelle Netzwerk wird über die **MAC-Adresse seines Default-Gateways** identifiziert — sie bleibt stabil, solange derselbe Router im Einsatz ist, unabhängig von SSID oder wechselndem DHCP-Subnetz. Ein Netzwerk gilt als vertrauenswürdig, wenn eine der folgenden Bedingungen zutrifft:
+
+1. Die Gateway-MAC steht in `trusted_networks` (Konfigurationsdatei oder `--trusted-networks`).
+2. Das Netzwerk wurde bereits einmal per Rückfrage bestätigt — das Ergebnis wird dauerhaft im globalen Cache gespeichert, sodass beim nächsten Besuch (auch nach Neustart) keine erneute Rückfrage nötig ist.
+3. `--auto-trust-networks` ist gesetzt: Die Rückfrage wird automatisch mit Ja beantwortet und das Ergebnis ebenfalls im Cache gemerkt.
+
+Ist keine der Bedingungen erfüllt:
+- Im **interaktiven Modus** fragt mac2ip auf stderr nach, ob der Scan in diesem Netzwerk erlaubt werden soll.
+- Im **`--json`-Modus** ist keine Rückfrage möglich (die Ausgabe darf nicht durch einen Prompt verunreinigt werden) — der Scan wird sicherheitshalber abgelehnt und ein Fehler zurückgegeben. Für unbeaufsichtigte Läufe in bekannten Netzwerken also entweder `trusted_networks` vorkonfigurieren oder einmalig interaktiv (ohne `--json`) bestätigen.
+
+Kann die Gateway-MAC nicht ermittelt werden (z. B. kein Default-Gateway vorhanden), wird der Scan ebenfalls abgelehnt, außer `--auto-trust-networks` ist gesetzt.
+
+---
+
+## Voraussetzungen
+
+Neben Rust/Cargo zur Laufzeit benötigt werden folgende System-Tools:
+
+- `ip` (Paket `iproute2`) — für Schritt 2 (`ip neigh`) und die automatische Subnetz-Erkennung.
+- `ping` (Paket `iputils`/`iputils-ping`) — für die Erreichbarkeitsprüfung.
+- `nmap` — für Schritt 3.
+- `sudo` — nur nötig, wenn Schritt 3 erreicht wird und das Programm nicht bereits als root läuft.
+
+Im `--json`-Modus wird für Schritt 3 ausschließlich `sudo -n` (nicht-interaktiv) verwendet, damit das Programm niemals interaktiv nach einem Passwort fragt und dadurch ein Skript blockiert. Ist keine gültige sudo-Sitzung/NOPASSWD-Regel vorhanden, wird Schritt 3 abgebrochen und das Ergebnis als "nicht gefunden" zurückgegeben. Im interaktiven (Nicht-JSON-)Modus darf `sudo` regulär nach einem Passwort fragen.
---
@@ -36,52 +141,42 @@ Dieses Template bietet eine vorkonfigurierte Umgebung für modernes Rust (Editio
│ └── workflows/
│ ├── main.yaml # CI/CD-Workflow für stabile Releases (main-Branch)
│ └── testing.yaml # CI/CD-Workflow für Pre-Releases (testing-Branch)
+├── packaging/
+│ ├── deb/postinst # Debian-Postinstall: legt /var/lib/mac2ip an
+│ └── arch/mac2ip.install # Arch-Install-Skriptlet: legt /var/lib/mac2ip an
├── scripts/
│ ├── get-build-number.py # Dynamische Ermittlung der nächsten Paket-Revisionsnummer
│ └── package-arch.py # Erstellung von Arch Linux .pkg.tar.zst Paketen
├── src/
-│ └── main.rs # Quellcode & Einstiegspunkt der Anwendung
-├── Cargo.toml # Cargo Manifest & Paketierungsmetadaten (deb, rpm, arch)
-├── LICENSE # Lizenzdatei (Standard: GPL-3.0-or-later)
-├── AGENTS.md # Richtlinien und Leitfaden für KI-Coding-Agenten
-└── README.md # Projektdokumentation
+│ ├── main.rs # Einstiegspunkt (CLI-Parsing, Wiring)
+│ ├── lib.rs # Modul-Wurzel (für Integrationstests)
+│ ├── cli.rs # clap-Kommandozeilen-Definition
+│ ├── config.rs # AppConfig + config-ctdra-Integration + CLI-Overlay
+│ ├── log.rs # JSON-Modus-bewusster logger-ctdra-Wrapper
+│ ├── mac.rs # MacAddress-Typ (Parsing/Kanonisierung)
+│ ├── cache.rs # Globaler Turso-Cache (MAC -> IP)
+│ ├── network.rs # ip neigh / ping / nmap: Exec- und Parse-Funktionen
+│ ├── resolver.rs # 3-Stufen-Algorithmus (Orchestrierung)
+│ └── output.rs # Human- und JSON-Ausgabe
+├── tests/ # Integrationstests (pure Parsing-/Logik-Funktionen)
+├── Cargo.toml # Cargo Manifest & Paketierungsmetadaten (deb, rpm, arch)
+├── LICENSE # Lizenzdatei (Standard: GPL-3.0-or-later)
+├── AGENTS.md # Richtlinien und Leitfaden für KI-Coding-Agenten
+└── README.md # Projektdokumentation
```
---
-## Checkliste zur Verwendung als Template
-
-Wenn du ein neues Projekt aus diesem Template erstellst, gehe folgende Schritte durch:
-
-1. **`Cargo.toml` anpassen**:
- - `name`: Den Namen deiner Anwendung setzen.
- - `version`: Initiale Version festlegen (z. B. `0.1.0`).
- - `authors`, `repository`, `description`, `license`: Projektdaten eintragen.
- - Paketierungsabschnitte prüfen:
- - `[package.metadata.deb]`: `maintainer`, `copyright`, `section`, `extended-description` setzen.
- - `[package.metadata.generate-rpm]`: `requires` anpassen.
- - `[package.metadata.arch]`: `depends` und `optdepends` anpassen.
-2. **`src/` implementieren**:
- - Eigene Anwendungslogik in `src/main.rs` (bzw. Modulen / `src/lib.rs`) implementieren.
- - Tests in `src/` oder `tests/` ergänzen.
-3. **`.cargo/config.toml` prüfen**:
- - Falls crates.io statt einer privaten Registry genutzt werden soll, den Standard-Registry-Eintrag anpassen oder auskommentieren.
-4. **CI/CD Secrets konfigurieren**:
- - In den Repository-Einstellungen von Gitea/Forgejo ein Secret `PACKAGE_TOKEN` (bzw. `GITEA_TOKEN`) mit Rechten für Pakete und Releases hinterlegen.
-5. **Dokumentation aktualisieren**:
- - `README.md` an die konkrete Funktionsweise deiner Anwendung anpassen.
-
----
-
## Lokale Entwicklung
### Voraussetzungen
- **Rust & Cargo** (aktuelle Stable-Version, Edition 2024 unterstützt)
- **Python 3** (für Hilfsskripte in `scripts/`)
+- Zur Laufzeit: `ip`, `ping`, `nmap`, `sudo` (siehe "Voraussetzungen" oben)
- Für Cross-Compilation (optional):
- - `rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu i686-unknown-linux-gnu`
- - Cross-Toolchains: `gcc-aarch64-linux-gnu`, `gcc-i686-linux-gnu`
+ - `rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu`
+ - Cross-Toolchains: `gcc-aarch64-linux-gnu`
- Für lokales Paketieren (optional):
- `cargo install cargo-deb cargo-generate-rpm`
@@ -126,6 +221,7 @@ Erstellt native Arch Linux-Pakete (`.pkg.tar.zst`), ohne dass `makepkg` oder ein
- Liest Konfiguration aus `[package.metadata.arch]` in `Cargo.toml`.
- Installiert die Binary nach `/usr/bin/`, sowie `LICENSE` und `README.md` nach `/usr/share/`.
- Erzeugt eine standardkonforme `.PKGINFO`-Datei und komprimiert das Paket mit `zstandard`.
+- Unterstützt optional ein Install-Skriptlet (`[package.metadata.arch].install_script`), das pacman-konform als `.install` mit `post_install()`/`post_upgrade()` eingebettet wird (bei mac2ip: legt `/var/lib/mac2ip` an).
- Parameter:
- `--target`: Rust Target-Triple (z. B. `x86_64-unknown-linux-gnu`)
- `--arch`: Zielarchitektur (z. B. `x86_64`, `aarch64`, `i686`)
@@ -144,5 +240,4 @@ Erstellt native Arch Linux-Pakete (`.pkg.tar.zst`), ohne dass `makepkg` oder ein
## Lizenz
-Dieses Template steht standardmäßig unter der [GPL-3.0-or-later](LICENSE)-Lizenz. Die Lizenz kann bei Bedarf in `LICENSE` und `Cargo.toml` angepasst werden.
-
+mac2ip steht unter der [GPL-3.0-or-later](LICENSE)-Lizenz.
diff --git a/packaging/arch/mac2ip.install b/packaging/arch/mac2ip.install
new file mode 100644
index 0000000..4a31851
--- /dev/null
+++ b/packaging/arch/mac2ip.install
@@ -0,0 +1,8 @@
+post_install() {
+ mkdir -p /var/lib/mac2ip
+ chmod 0777 /var/lib/mac2ip
+}
+
+post_upgrade() {
+ post_install
+}
diff --git a/packaging/deb/postinst b/packaging/deb/postinst
new file mode 100755
index 0000000..4d8f64f
--- /dev/null
+++ b/packaging/deb/postinst
@@ -0,0 +1,7 @@
+#!/bin/sh
+set -e
+
+mkdir -p /var/lib/mac2ip
+chmod 0777 /var/lib/mac2ip
+
+exit 0
diff --git a/scripts/package-arch.py b/scripts/package-arch.py
index d03aeee..13cf639 100755
--- a/scripts/package-arch.py
+++ b/scripts/package-arch.py
@@ -87,6 +87,7 @@ def build_package(target_triple=None, target_arch=None, pkgrel=None):
depends = arch_meta.get("depends", ["gcc-libs", "glibc"])
optdepends = arch_meta.get("optdepends", [])
+ install_rel_path = arch_meta.get("install_script")
with tempfile.TemporaryDirectory() as build_dir:
bin_dir = os.path.join(build_dir, "usr/bin")
@@ -102,6 +103,14 @@ def build_package(target_triple=None, target_arch=None, pkgrel=None):
if os.path.exists("README.md"):
subprocess.run(["install", "-m", "644", "README.md", f"{doc_dir}/README.md"], check=True)
+ has_install_scriptlet = False
+ if install_rel_path and os.path.exists(install_rel_path):
+ has_install_scriptlet = True
+ subprocess.run(
+ ["install", "-m", "644", install_rel_path, os.path.join(build_dir, ".INSTALL")],
+ check=True,
+ )
+
installed_size = subprocess.check_output(["du", "-sb", build_dir]).decode().split()[0]
builddate = str(int(time.time()))
@@ -128,7 +137,11 @@ def build_package(target_triple=None, target_arch=None, pkgrel=None):
os.makedirs("target/arch", exist_ok=True)
output_file = os.path.abspath(f"target/arch/{name}-{version}-{pkgrel}-{arch}.pkg.tar.zst")
- subprocess.run(["tar", "--zstd", "-cf", output_file, ".PKGINFO", "usr"], cwd=build_dir, check=True)
+ tar_members = [".PKGINFO"]
+ if has_install_scriptlet:
+ tar_members.append(".INSTALL")
+ tar_members.append("usr")
+ subprocess.run(["tar", "--zstd", "-cf", output_file, *tar_members], cwd=build_dir, check=True)
print(f"Arch-Paket erfolgreich erstellt: {output_file}")
diff --git a/src/cache.rs b/src/cache.rs
new file mode 100644
index 0000000..6907551
--- /dev/null
+++ b/src/cache.rs
@@ -0,0 +1,258 @@
+//! Globaler, systemweiter Cache (MAC -> IP) auf Basis der `turso`-Crate (lokale Datei).
+//!
+//! Der Cache liegt absichtlich an einem einzigen, für alle Nutzer gemeinsamen Pfad
+//! (Standard: `/var/lib/mac2ip/cache.db`). Ist dieser Pfad nicht anlegbar/beschreibbar
+//! (z. B. bei einem Entwicklungslauf ohne vorherige Paketinstallation), wird der Cache
+//! für den aktuellen Lauf einfach deaktiviert (Warnung, best-effort) - es gibt bewusst
+//! KEINEN Fallback auf einen Pro-User-Pfad, da der Cache explizit global sein soll.
+
+use std::net::IpAddr;
+use std::path::{Path, PathBuf};
+
+use crate::mac::MacAddress;
+
+#[cfg(unix)]
+use std::os::unix::fs::PermissionsExt;
+
+pub struct CacheEntry {
+ pub ip: IpAddr,
+ #[allow(dead_code)]
+ pub source: String,
+ pub updated_at: i64,
+}
+
+pub struct Cache {
+ conn: turso::Connection,
+}
+
+impl Cache {
+ async fn init_schema(&self) -> turso::Result<()> {
+ self.conn
+ .execute(
+ "CREATE TABLE IF NOT EXISTS mac_ip_cache (\
+ mac TEXT PRIMARY KEY, \
+ ip TEXT NOT NULL, \
+ source TEXT NOT NULL, \
+ updated_at INTEGER NOT NULL\
+ )",
+ (),
+ )
+ .await?;
+ self.conn
+ .execute(
+ "CREATE TABLE IF NOT EXISTS trusted_networks (\
+ gateway_mac TEXT PRIMARY KEY, \
+ trusted_at INTEGER NOT NULL\
+ )",
+ (),
+ )
+ .await?;
+ Ok(())
+ }
+
+ /// Best-effort: gibt bei jedem Fehler `false` zurück (nur eine Warnung wird geloggt) -
+ /// ein Cache-Lesefehler darf ein zuvor bereits bestätigtes Netzwerk nicht plötzlich
+ /// als unbekannt erscheinen lassen, aber sicherheitshalber auch nicht automatisch
+ /// als vertrauenswürdig gelten lassen; der Aufrufer fragt in diesem Fall erneut nach.
+ pub async fn is_trusted_network(&self, gateway_mac: &MacAddress) -> bool {
+ let key = gateway_mac.to_lower_colon();
+ match self
+ .conn
+ .query(
+ "SELECT 1 FROM trusted_networks WHERE gateway_mac = ?1",
+ (key,),
+ )
+ .await
+ {
+ Ok(mut rows) => matches!(rows.next().await, Ok(Some(_))),
+ Err(e) => {
+ crate::log::warn(
+ "cache",
+ &format!("Cache-Lesefehler (trusted_networks): {e}"),
+ );
+ false
+ }
+ }
+ }
+
+ /// Best-effort Upsert: merkt sich ein Netzwerk (identifiziert über die Gateway-MAC)
+ /// dauerhaft als vertrauenswürdig für zukünftige nmap-Scans.
+ pub async fn trust_network(&self, gateway_mac: &MacAddress, now: i64) {
+ let key = gateway_mac.to_lower_colon();
+ if let Err(e) = self
+ .conn
+ .execute(
+ "INSERT INTO trusted_networks (gateway_mac, trusted_at) VALUES (?1, ?2) \
+ ON CONFLICT(gateway_mac) DO UPDATE SET trusted_at = excluded.trusted_at",
+ (key, now),
+ )
+ .await
+ {
+ crate::log::warn(
+ "cache",
+ &format!("Cache-Schreibfehler (trusted_networks, ignoriert): {e}"),
+ );
+ }
+ }
+
+ /// Best-effort: gibt bei jedem Fehler `None` zurück (nur eine Warnung wird geloggt).
+ pub async fn get(&self, mac: &MacAddress) -> Option {
+ let key = mac.to_lower_colon();
+ let mut rows = match self
+ .conn
+ .query(
+ "SELECT ip, source, updated_at FROM mac_ip_cache WHERE mac = ?1",
+ (key,),
+ )
+ .await
+ {
+ Ok(rows) => rows,
+ Err(e) => {
+ crate::log::warn("cache", &format!("Cache-Lesefehler: {e}"));
+ return None;
+ }
+ };
+
+ match rows.next().await {
+ Ok(Some(row)) => {
+ let ip_str: String = row.get(0).ok()?;
+ let source: String = row.get(1).ok()?;
+ let updated_at: i64 = row.get(2).ok()?;
+ ip_str.parse().ok().map(|ip| CacheEntry {
+ ip,
+ source,
+ updated_at,
+ })
+ }
+ Ok(None) => None,
+ Err(e) => {
+ crate::log::warn("cache", &format!("Cache-Lesefehler: {e}"));
+ None
+ }
+ }
+ }
+
+ /// Best-effort Upsert: Fehler werden nur geloggt, niemals propagiert - ein
+ /// Cache-Schreibfehler darf den 3-Stufen-Lookup nie zum Scheitern bringen.
+ pub async fn upsert(&self, mac: &MacAddress, ip: IpAddr, source: &str, now: i64) {
+ let key = mac.to_lower_colon();
+ if let Err(e) = self
+ .conn
+ .execute(
+ "INSERT INTO mac_ip_cache (mac, ip, source, updated_at) VALUES (?1, ?2, ?3, ?4) \
+ ON CONFLICT(mac) DO UPDATE SET ip = excluded.ip, source = excluded.source, updated_at = excluded.updated_at",
+ (key, ip.to_string(), source.to_string(), now),
+ )
+ .await
+ {
+ crate::log::warn("cache", &format!("Cache-Schreibfehler (ignoriert): {e}"));
+ }
+ }
+}
+
+/// Reine, injectable-Zeit-Funktion - unit-testbar ohne echte Systemzeit.
+pub fn is_expired(updated_at: i64, now: i64, ttl_seconds: u64) -> bool {
+ now.saturating_sub(updated_at) as u64 > ttl_seconds
+}
+
+fn ensure_cache_dir(dir: &Path) -> Result<(), String> {
+ if !dir.exists() {
+ std::fs::create_dir_all(dir).map_err(|e| {
+ format!(
+ "Verzeichnis '{}' konnte nicht angelegt werden: {e}",
+ dir.display()
+ )
+ })?;
+ // create_dir_all() unterliegt dem Prozess-Umask; explizites chmod ist nötig, um
+ // wirklich 0777 zu erreichen (Verzeichnis-Rechte steuern nicht die Umask neuer Dateien).
+ // Wird nur beim Neuanlegen ausgeführt; bei existierenden Verzeichnissen würde ein
+ // unprivilegierter Aufruf auf root-eigenen Pfaden mit EPERM fehlschlagen.
+ #[cfg(unix)]
+ std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o777))
+ .map_err(|e| format!("Verzeichnis-Rechte konnten nicht gesetzt werden: {e}"))?;
+ }
+ Ok(())
+}
+
+fn chmod_cache_sidecars(db_path: &Path) {
+ #[cfg(unix)]
+ for suffix in ["", "-wal", "-shm", "-journal"] {
+ let p = PathBuf::from(format!("{}{suffix}", db_path.display()));
+ if p.exists() {
+ let _ = std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o666));
+ }
+ }
+}
+
+/// Best-effort Cache-Öffnung: gibt `None` zurück (statt Fehler), wenn der Cache aus
+/// irgendeinem Grund nicht verfügbar ist - der Aufrufer führt den 3-Stufen-Algorithmus
+/// dann einfach ohne Cache aus.
+pub async fn try_open_cache(db_path: &Path) -> Option {
+ if let Some(dir) = db_path.parent()
+ && let Err(reason) = ensure_cache_dir(dir)
+ {
+ crate::log::warn("cache", &format!("Cache deaktiviert: {reason}"));
+ return None;
+ }
+
+ let db = match turso::Builder::new_local(db_path.to_string_lossy().as_ref())
+ .build()
+ .await
+ {
+ Ok(db) => db,
+ Err(e) => {
+ crate::log::warn(
+ "cache",
+ &format!("Cache-Datenbank konnte nicht geöffnet werden: {e}"),
+ );
+ return None;
+ }
+ };
+
+ let conn = match db.connect() {
+ Ok(c) => c,
+ Err(e) => {
+ crate::log::warn("cache", &format!("Cache-Verbindung fehlgeschlagen: {e}"));
+ return None;
+ }
+ };
+
+ let cache = Cache { conn };
+ if let Err(e) = cache.init_schema().await {
+ crate::log::warn(
+ "cache",
+ &format!("Cache-Schema konnte nicht initialisiert werden: {e}"),
+ );
+ return None;
+ }
+
+ chmod_cache_sidecars(db_path);
+ Some(cache)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_ensure_cache_dir_existing_directory_does_not_fail() {
+ // Bestehende Systemverzeichnisse (die einem unprivilegierten Nutzer nicht gehören)
+ // dürfen beim Überprüfen keinen EPERM-Fehler werfen.
+ assert!(ensure_cache_dir(Path::new("/tmp")).is_ok());
+ }
+
+ #[test]
+ fn test_ensure_cache_dir_creates_new_directory() {
+ let temp_dir = std::env::temp_dir().join(format!(
+ "mac2ip_test_{}",
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_nanos()
+ ));
+ assert!(!temp_dir.exists());
+ assert!(ensure_cache_dir(&temp_dir).is_ok());
+ assert!(temp_dir.exists());
+ let _ = std::fs::remove_dir(&temp_dir);
+ }
+}
diff --git a/src/cli.rs b/src/cli.rs
new file mode 100644
index 0000000..024e104
--- /dev/null
+++ b/src/cli.rs
@@ -0,0 +1,89 @@
+//! Kommandozeilen-Interface.
+
+use std::path::PathBuf;
+
+use clap::{Parser, ValueEnum};
+
+use crate::mac::MacAddress;
+
+#[derive(Parser, Debug)]
+#[command(
+ name = "mac2ip",
+ author,
+ version,
+ about = "Findet zuverlässig die aktuelle IP-Adresse zu einer MAC-Adresse im lokalen Netzwerk",
+ long_about = None
+)]
+pub struct Cli {
+ /// MAC-Adresse, deren aktuelle IP-Adresse ermittelt werden soll (z. B. aa:bb:cc:dd:ee:ff)
+ pub mac: MacAddress,
+
+ /// Gibt das Ergebnis als einzeiliges JSON-Objekt aus; unterdrückt alle sonstigen Log-Ausgaben
+ #[arg(long)]
+ pub json: bool,
+
+ /// Benutzerdefinierter Pfad zur Konfigurationsdatei
+ #[arg(long)]
+ pub config: Option,
+
+ /// Logging-Level (error, warn, info, debug)
+ #[arg(long, value_enum, env = "MAC2IP_LOG_LEVEL")]
+ pub log_level: Option,
+
+ /// Cache-TTL in Sekunden (Standard: 1800)
+ #[arg(long, env = "MAC2IP_CACHE_TTL_SECONDS")]
+ pub cache_ttl_seconds: Option,
+
+ /// Pfad zur globalen Cache-Datenbankdatei (Standard: /var/lib/mac2ip/cache.db)
+ #[arg(long, env = "MAC2IP_CACHE_DB_PATH")]
+ pub cache_db_path: Option,
+
+ /// Timeout in Sekunden für einen einzelnen nmap-Subnetz-Scan (Standard: 120)
+ #[arg(long, env = "MAC2IP_NMAP_TIMEOUT_SECONDS")]
+ pub nmap_timeout_seconds: Option,
+
+ /// Kommagetrennte Liste von CIDR-Subnetzen für den nmap-Scan (überschreibt Auto-Erkennung)
+ #[arg(long, env = "MAC2IP_NETWORKS", value_delimiter = ',')]
+ pub networks: Option>,
+
+ /// Kommagetrennte Liste von Gateway-MAC-Adressen, deren Netzwerke ohne Rückfrage für
+ /// nmap-Scans (Schritt 3) vertraut werden (überschreibt die Konfigurationsdatei vollständig)
+ #[arg(long, env = "MAC2IP_TRUSTED_NETWORKS", value_delimiter = ',')]
+ pub trusted_networks: Option>,
+
+ /// Beantwortet die "nmap-Scan in diesem Netzwerk erlauben?"-Rückfrage vor Schritt 3
+ /// automatisch mit Ja (und merkt sich das Netzwerk dauerhaft im Cache), statt
+ /// interaktiv nachzufragen bzw. im --json-Modus den Scan abzulehnen
+ #[arg(long)]
+ pub auto_trust_networks: bool,
+}
+
+#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
+pub enum LogLevelArg {
+ Error,
+ Warn,
+ Info,
+ Debug,
+}
+
+impl LogLevelArg {
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ Self::Error => "error",
+ Self::Warn => "warn",
+ Self::Info => "info",
+ Self::Debug => "debug",
+ }
+ }
+}
+
+impl From for logger_ctdra::LogLevel {
+ fn from(value: LogLevelArg) -> Self {
+ match value {
+ LogLevelArg::Error => logger_ctdra::LogLevel::Error,
+ LogLevelArg::Warn => logger_ctdra::LogLevel::Warn,
+ LogLevelArg::Info => logger_ctdra::LogLevel::Info,
+ LogLevelArg::Debug => logger_ctdra::LogLevel::Debug,
+ }
+ }
+}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..abbc921
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,116 @@
+//! Anwendungskonfiguration: persistente Datei (via `config-ctdra`) + CLI-Overlay.
+
+use std::path::{Path, PathBuf};
+
+use serde::{Deserialize, Serialize};
+
+use crate::mac::MacAddress;
+
+pub const DEFAULT_CACHE_TTL_SECONDS: u64 = 1800; // 30 Min: Ping-Check ist die primäre
+// Absicherung gegen veraltete Einträge, die TTL ist nur eine zusätzliche Absicherung
+// gegen den Fall, dass eine alte IP inzwischen an ein anderes, ebenfalls
+// erreichbares Gerät vergeben wurde.
+pub const DEFAULT_CACHE_DB_PATH: &str = "/var/lib/mac2ip/cache.db";
+pub const DEFAULT_LOG_LEVEL: &str = "info";
+pub const DEFAULT_NMAP_TIMEOUT_SECONDS: u64 = 120;
+
+fn default_cache_ttl_seconds() -> u64 {
+ DEFAULT_CACHE_TTL_SECONDS
+}
+
+fn default_cache_db_path() -> PathBuf {
+ PathBuf::from(DEFAULT_CACHE_DB_PATH)
+}
+
+fn default_log_level() -> String {
+ DEFAULT_LOG_LEVEL.to_string()
+}
+
+fn default_nmap_timeout_seconds() -> u64 {
+ DEFAULT_NMAP_TIMEOUT_SECONDS
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+pub struct AppConfig {
+ #[serde(default = "default_cache_ttl_seconds")]
+ pub cache_ttl_seconds: u64,
+ #[serde(default = "default_cache_db_path")]
+ pub cache_db_path: PathBuf,
+ #[serde(default = "default_log_level")]
+ pub log_level: String,
+ #[serde(default = "default_nmap_timeout_seconds")]
+ pub nmap_timeout_seconds: u64,
+ #[serde(default)]
+ pub networks: Vec,
+ /// Gateway-MAC-Adressen von Netzwerken, in denen ein nmap-Scan (Schritt 3) ohne
+ /// interaktive Rückfrage erlaubt ist. Wird um per Rückfrage/`--auto-trust-networks`
+ /// bestätigte Netzwerke ergänzt, die im Cache gespeichert werden (siehe `src/trust.rs`).
+ #[serde(default)]
+ pub trusted_networks: Vec,
+}
+
+impl Default for AppConfig {
+ fn default() -> Self {
+ Self {
+ cache_ttl_seconds: DEFAULT_CACHE_TTL_SECONDS,
+ cache_db_path: PathBuf::from(DEFAULT_CACHE_DB_PATH),
+ log_level: DEFAULT_LOG_LEVEL.to_string(),
+ nmap_timeout_seconds: DEFAULT_NMAP_TIMEOUT_SECONDS,
+ networks: Vec::new(),
+ trusted_networks: Vec::new(),
+ }
+ }
+}
+
+impl AppConfig {
+ /// Reine, testbare Funktion: validiert die konfigurierten vertrauten Netzwerke.
+ /// Nicht als MAC-Adresse parsbare Einträge werden übersprungen (kein harter Fehler,
+ /// da eine fehlerhafte Config-Zeile den Lookup nicht insgesamt scheitern lassen soll).
+ pub fn trusted_network_macs(&self) -> Vec {
+ self.trusted_networks
+ .iter()
+ .filter_map(|s| MacAddress::parse(s).ok())
+ .collect()
+ }
+}
+
+/// Initialisiert den Konfigurationspfad bei `config-ctdra` (Standardname "config",
+/// optionaler expliziter Pfad via `--config`).
+pub fn init_config_path(custom_path: Option<&Path>) {
+ config_ctdra::set_config_name("config");
+ if let Some(path) = custom_path {
+ config_ctdra::set_custom_path(path.to_path_buf());
+ }
+}
+
+pub fn get_config_file_path() -> PathBuf {
+ config_ctdra::get_config_path()
+}
+
+pub fn load_config() -> AppConfig {
+ config_ctdra::load_config::()
+}
+
+/// Reine, testbare Overlay-Funktion: überschreibt Konfigurationsfelder mit
+/// expliziten CLI-Werten (clap hat CLI>ENV bereits selbst aufgelöst; hier wird
+/// nur noch "vorhanden > Datei/Default" angewendet).
+pub fn apply_cli_overrides(config: &mut AppConfig, cli: &crate::cli::Cli) {
+ if let Some(level) = &cli.log_level {
+ config.log_level = level.as_str().to_string();
+ }
+ if let Some(ttl) = cli.cache_ttl_seconds {
+ config.cache_ttl_seconds = ttl;
+ }
+ if let Some(path) = &cli.cache_db_path {
+ config.cache_db_path = path.clone();
+ }
+ if let Some(timeout) = cli.nmap_timeout_seconds {
+ config.nmap_timeout_seconds = timeout;
+ }
+ if let Some(networks) = &cli.networks {
+ config.networks = networks.clone();
+ }
+ if let Some(trusted) = &cli.trusted_networks {
+ config.trusted_networks = trusted.iter().map(MacAddress::to_lower_colon).collect();
+ }
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..ed8c7c7
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,18 @@
+//! Fehlertypen für mac2ip.
+
+use crate::mac::MacAddressError;
+
+#[derive(Debug, thiserror::Error)]
+pub enum Mac2IpError {
+ #[error("keine IP-Adresse für MAC '{mac}' gefunden")]
+ NotFound { mac: String },
+ #[error(
+ "nmap-Scan abgelehnt: Netzwerk nicht vertrauenswürdig{}",
+ .gateway_mac.as_ref().map(|m| format!(" (Gateway-MAC {m})")).unwrap_or_default()
+ )]
+ UntrustedNetwork { gateway_mac: Option },
+ #[error("MAC-Adresse ungültig: {0}")]
+ InvalidMac(#[from] MacAddressError),
+ #[error("E/A-Fehler: {0}")]
+ Io(#[from] std::io::Error),
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..4e4142a
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,13 @@
+//! mac2ip - findet zuverlässig die aktuelle IP-Adresse zu einer MAC-Adresse
+//! im lokalen Netzwerk (Cache -> `ip neigh` -> `nmap`).
+
+pub mod cache;
+pub mod cli;
+pub mod config;
+pub mod error;
+pub mod log;
+pub mod mac;
+pub mod network;
+pub mod output;
+pub mod resolver;
+pub mod trust;
diff --git a/src/log.rs b/src/log.rs
new file mode 100644
index 0000000..129b2b7
--- /dev/null
+++ b/src/log.rs
@@ -0,0 +1,43 @@
+//! JSON-Modus-bewusster Wrapper um `logger-ctdra`.
+//!
+//! Im `--json`-Modus dürfen keinerlei Log-Ausgaben (auch keine Fehler/Warnungen)
+//! auf stdout/stderr erscheinen, da nur ein einzelnes JSON-Objekt erwartet wird.
+//! Alle Module rufen ausschließlich diese Funktionen auf, niemals `logger_ctdra`
+//! direkt, damit die Unterdrückung an genau einer Stelle greift.
+
+use std::sync::atomic::{AtomicBool, Ordering};
+
+static JSON_MODE: AtomicBool = AtomicBool::new(false);
+
+/// Muss als allererste Logger-bezogene Aktion in main() aufgerufen werden.
+pub fn set_json_mode(enabled: bool) {
+ JSON_MODE.store(enabled, Ordering::Relaxed);
+}
+
+pub fn is_json_mode() -> bool {
+ JSON_MODE.load(Ordering::Relaxed)
+}
+
+pub fn error(tag: &str, message: &str) {
+ if !is_json_mode() {
+ logger_ctdra::error(tag, message);
+ }
+}
+
+pub fn warn(tag: &str, message: &str) {
+ if !is_json_mode() {
+ logger_ctdra::warn(tag, message);
+ }
+}
+
+pub fn info(tag: &str, message: &str) {
+ if !is_json_mode() {
+ logger_ctdra::info(tag, message);
+ }
+}
+
+pub fn debug(tag: &str, message: &str) {
+ if !is_json_mode() {
+ logger_ctdra::debug(tag, message);
+ }
+}
diff --git a/src/mac.rs b/src/mac.rs
new file mode 100644
index 0000000..7e81c30
--- /dev/null
+++ b/src/mac.rs
@@ -0,0 +1,77 @@
+//! Repräsentation und Parsing von MAC-Adressen.
+
+use std::fmt;
+use std::str::FromStr;
+
+/// Kanonische Darstellung einer MAC-Adresse (6 Bytes).
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct MacAddress([u8; 6]);
+
+#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
+pub enum MacAddressError {
+ #[error("ungültige MAC-Adresse: '{0}'")]
+ InvalidFormat(String),
+}
+
+impl MacAddress {
+ /// Parst eine MAC-Adresse aus ':' oder '-' getrennter Hex-Notation (case-insensitive).
+ pub fn parse(input: &str) -> Result {
+ let err = || MacAddressError::InvalidFormat(input.to_string());
+
+ let normalized = input.trim();
+ let separator = if normalized.contains(':') {
+ ':'
+ } else if normalized.contains('-') {
+ '-'
+ } else {
+ return Err(err());
+ };
+
+ let parts: Vec<&str> = normalized.split(separator).collect();
+ if parts.len() != 6 {
+ return Err(err());
+ }
+
+ let mut bytes = [0u8; 6];
+ for (i, part) in parts.iter().enumerate() {
+ if part.len() != 2 {
+ return Err(err());
+ }
+ bytes[i] = u8::from_str_radix(part, 16).map_err(|_| err())?;
+ }
+
+ Ok(Self(bytes))
+ }
+
+ /// Kleinschreibung, Doppelpunkt-getrennt (Vergleichsform, wie `ip neigh` sie ausgibt).
+ pub fn to_lower_colon(&self) -> String {
+ self.0
+ .iter()
+ .map(|b| format!("{b:02x}"))
+ .collect::>()
+ .join(":")
+ }
+
+ /// Großschreibung, Doppelpunkt-getrennt (wie nmap sie ausgibt).
+ pub fn to_upper_colon(&self) -> String {
+ self.0
+ .iter()
+ .map(|b| format!("{b:02X}"))
+ .collect::>()
+ .join(":")
+ }
+}
+
+impl FromStr for MacAddress {
+ type Err = MacAddressError;
+
+ fn from_str(s: &str) -> Result {
+ Self::parse(s)
+ }
+}
+
+impl fmt::Display for MacAddress {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.to_lower_colon())
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 79004d9..72d5ecf 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,13 +1,44 @@
-fn main() {
- // TODO: Hauptlogik der Anwendung implementieren
- println!("Hello, World!");
-}
+use std::process::ExitCode;
-#[cfg(test)]
-mod tests {
- #[test]
- fn it_works() {
- // TODO: Unit-Tests für die Anwendung schreiben
- assert_eq!(2 + 2, 4);
+use clap::Parser;
+
+use mac2ip::{cache, cli::Cli, config, log, output, resolver};
+
+#[tokio::main]
+async fn main() -> ExitCode {
+ let cli = Cli::parse();
+
+ // Muss vor jedem weiteren Logger-Aufruf geschehen, damit im --json-Modus
+ // wirklich keine einzige Log-Zeile ausgegeben wird.
+ log::set_json_mode(cli.json);
+
+ config::init_config_path(cli.config.as_deref());
+ let mut app_config = config::load_config();
+ config::apply_cli_overrides(&mut app_config, &cli);
+
+ match logger_ctdra::LogLevel::try_from(app_config.log_level.as_str()) {
+ Ok(level) => logger_ctdra::set_log_level(level),
+ Err(fallback) => logger_ctdra::set_log_level(fallback),
+ }
+
+ let cache = cache::try_open_cache(&app_config.cache_db_path).await;
+
+ match resolver::resolve(
+ &cli.mac,
+ &app_config,
+ cache.as_ref(),
+ cli.json,
+ cli.auto_trust_networks,
+ )
+ .await
+ {
+ Ok(result) => {
+ output::print_success(&result, cli.json);
+ ExitCode::SUCCESS
+ }
+ Err(err) => {
+ output::print_failure(&cli.mac, &err, cli.json);
+ ExitCode::FAILURE
+ }
}
}
diff --git a/src/network.rs b/src/network.rs
new file mode 100644
index 0000000..2b06331
--- /dev/null
+++ b/src/network.rs
@@ -0,0 +1,255 @@
+//! Netzwerk-Interaktion: Auto-Erkennung lokaler Subnetze, `ip neigh`, Ping-Erreichbarkeit
+//! und der nmap-Scan (Schritt 3). Bewusst getrennt in dünne, das System aufrufende
+//! Exec-Funktionen und reine, unit-testbare Parse-Funktionen.
+
+use std::net::IpAddr;
+use std::time::Duration;
+
+use crate::mac::MacAddress;
+
+/// Schnelle Liveness-Probe, absichtlich kein Konfigurationsfeld (kein Nutzer-Tuning nötig).
+const PING_TIMEOUT_SECS: u32 = 1;
+
+// ---------------------------------------------------------------------------
+// Exec-Funktionen (führen echte Subprozesse aus)
+// ---------------------------------------------------------------------------
+
+pub async fn run_ip_neigh_show() -> std::io::Result {
+ let out = tokio::process::Command::new("ip")
+ .args(["neigh", "show"])
+ .output()
+ .await?;
+ Ok(String::from_utf8_lossy(&out.stdout).into_owned())
+}
+
+pub async fn run_ip_route_show_scope_link() -> std::io::Result {
+ let out = tokio::process::Command::new("ip")
+ .args(["-4", "route", "show", "scope", "link"])
+ .output()
+ .await?;
+ Ok(String::from_utf8_lossy(&out.stdout).into_owned())
+}
+
+pub async fn run_ip_route_show_default() -> std::io::Result {
+ let out = tokio::process::Command::new("ip")
+ .args(["-4", "route", "show", "default"])
+ .output()
+ .await?;
+ Ok(String::from_utf8_lossy(&out.stdout).into_owned())
+}
+
+/// Ermittelt die MAC-Adresse des aktuellen Default-Gateways - dient als stabiler
+/// Identifikator für "dieses Netzwerk" (bleibt bei DHCP-Subnetzwechsel gleich,
+/// solange derselbe Router im Einsatz ist), unabhängig von SSID/CIDR.
+/// Gibt `None` zurück, wenn kein Default-Gateway existiert oder dessen MAC nicht
+/// ermittelt werden kann (z. B. kein Nachbar-Eintrag trotz Ping).
+pub async fn detect_default_gateway_mac() -> Option {
+ let route_output = run_ip_route_show_default().await.ok()?;
+ let gateway_ip = parse_default_gateway_ip(&route_output)?;
+
+ // Erzwingt best-effort einen frischen Nachbar-Tabelleneintrag; Ergebnis wird
+ // ignoriert, da `ip neigh` auch von einem veralteten/STALE-Eintrag profitiert.
+ let _ = ping_check(gateway_ip).await;
+
+ let neigh_output = run_ip_neigh_show().await.ok()?;
+ parse_ip_neigh_mac_for_ip(&neigh_output, gateway_ip)
+}
+
+pub async fn ping_check(ip: IpAddr) -> bool {
+ tokio::process::Command::new("ping")
+ .args([
+ "-c",
+ "1",
+ "-W",
+ &PING_TIMEOUT_SECS.to_string(),
+ &ip.to_string(),
+ ])
+ // stdout/stderr von `ping` sind für uns irrelevant (nur der Exit-Code zählt) und
+ // dürfen insbesondere im --json-Modus nicht in unseren eigenen Output durchsickern.
+ .stdout(std::process::Stdio::null())
+ .stderr(std::process::Stdio::null())
+ .status()
+ .await
+ .map(|s| s.success())
+ .unwrap_or(false)
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum SudoMode {
+ /// Bereits root - nmap wird ohne sudo-Prefix aufgerufen.
+ None,
+ /// sudo darf interaktiv nach einem Passwort fragen.
+ Interactive,
+ /// sudo darf NICHT interaktiv nach einem Passwort fragen (`sudo -n`).
+ NonInteractive,
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum NmapRunError {
+ #[error("sudo-Zugangsdaten nicht verfügbar (nicht-interaktiver Modus)")]
+ SudoUnavailable,
+ #[error("nmap-Scan hat das Zeitlimit überschritten")]
+ Timeout,
+ #[error("E/A-Fehler: {0}")]
+ Io(#[from] std::io::Error),
+}
+
+pub async fn run_nmap_scan(
+ cidr: &str,
+ timeout_secs: u64,
+ mode: SudoMode,
+) -> Result {
+ let mut cmd = match mode {
+ SudoMode::None => {
+ let mut c = tokio::process::Command::new("nmap");
+ c.arg("-sn").arg("-n").arg(cidr);
+ c
+ }
+ SudoMode::Interactive => {
+ let mut c = tokio::process::Command::new("sudo");
+ c.args(["nmap", "-sn", "-n", cidr]);
+ c
+ }
+ SudoMode::NonInteractive => {
+ let mut c = tokio::process::Command::new("sudo");
+ c.args(["-n", "nmap", "-sn", "-n", cidr]);
+ c
+ }
+ };
+ // Erzwingt englische Fehlermeldungen, damit die "password"-Erkennung unten
+ // unabhängig von der System-Locale funktioniert (z. B. "Passwort ist notwendig"
+ // auf einem deutschen System würde sonst nicht erkannt werden).
+ cmd.env("LC_ALL", "C").env("LANG", "C");
+ cmd.kill_on_drop(true);
+ // Eigene Prozessgruppe: `kill_on_drop`/Child::kill() signalisiert bei einem
+ // Timeout nur den direkten Kind-Prozess. Im Sudo-Fall ist das `sudo` selbst,
+ // nicht das von `sudo` geforkte (als root laufende) `nmap`. Ohne eigene
+ // Prozessgruppe würde `sudo` beim SIGKILL sterben, während `nmap` als
+ // root-Waise weiterläuft und den Scan fortsetzt.
+ cmd.process_group(0);
+
+ let child = cmd.spawn()?;
+ let pgid = child.id();
+
+ let out =
+ match tokio::time::timeout(Duration::from_secs(timeout_secs), child.wait_with_output())
+ .await
+ {
+ Ok(out) => out?,
+ Err(_) => {
+ if let Some(pgid) = pgid {
+ // Negative PID = Signal an die gesamte Prozessgruppe (sudo + nmap).
+ let _ = tokio::process::Command::new("kill")
+ .arg("-KILL")
+ .arg(format!("-{pgid}"))
+ .output()
+ .await;
+ }
+ return Err(NmapRunError::Timeout);
+ }
+ };
+
+ if !out.status.success() {
+ let stderr = String::from_utf8_lossy(&out.stderr);
+ if stderr.to_lowercase().contains("password") {
+ return Err(NmapRunError::SudoUnavailable);
+ }
+ return Err(NmapRunError::Io(std::io::Error::other(stderr.into_owned())));
+ }
+
+ Ok(String::from_utf8_lossy(&out.stdout).into_owned())
+}
+
+// ---------------------------------------------------------------------------
+// Reine Parse-Funktionen (kein Subprozess, vollständig unit-testbar)
+// ---------------------------------------------------------------------------
+
+/// Parst `ip neigh show`-Ausgabe, z. B. Zeilen wie
+/// "192.168.1.5 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE".
+/// Gibt die IP zurück, sobald eine Zeile mit passender `lladdr`-MAC gefunden wird.
+pub fn parse_ip_neigh_output(output: &str, mac: &MacAddress) -> Option {
+ let target = mac.to_lower_colon();
+ for line in output.lines() {
+ let tokens: Vec<&str> = line.split_whitespace().collect();
+ if tokens.is_empty() {
+ continue;
+ }
+ if let Some(pos) = tokens.iter().position(|&t| t == "lladdr")
+ && let Some(&found_mac) = tokens.get(pos + 1)
+ && found_mac.eq_ignore_ascii_case(&target)
+ && let Ok(ip) = tokens[0].parse()
+ {
+ return Some(ip);
+ }
+ }
+ None
+}
+
+/// Parst `ip -4 route show default`, z. B. "default via 192.168.1.1 dev eth0 proto dhcp
+/// metric 100". Gibt die Gateway-IP zurück (Token direkt nach "via").
+pub fn parse_default_gateway_ip(output: &str) -> Option {
+ output.lines().find_map(|line| {
+ let tokens: Vec<&str> = line.split_whitespace().collect();
+ let pos = tokens.iter().position(|&t| t == "via")?;
+ tokens.get(pos + 1)?.parse().ok()
+ })
+}
+
+/// Parst `ip neigh show`-Ausgabe und gibt die `lladdr`-MAC des Eintrags zurück, dessen
+/// erste Spalte (IP) mit `target_ip` übereinstimmt. Kehrbild zu [`parse_ip_neigh_output`].
+pub fn parse_ip_neigh_mac_for_ip(output: &str, target_ip: IpAddr) -> Option {
+ for line in output.lines() {
+ let tokens: Vec<&str> = line.split_whitespace().collect();
+ if tokens.first().and_then(|t| t.parse::().ok()) != Some(target_ip) {
+ continue;
+ }
+ if let Some(pos) = tokens.iter().position(|&t| t == "lladdr")
+ && let Some(&mac_str) = tokens.get(pos + 1)
+ && let Ok(mac) = mac_str.parse()
+ {
+ return Some(mac);
+ }
+ }
+ None
+}
+
+/// Extrahiert lokale CIDR-Subnetze aus `ip -4 route show scope link`.
+/// Die CIDR ist bereits das erste whitespace-getrennte Token pro passender Zeile.
+/// Überspringt `default`-Zeilen und das Loopback-Interface.
+pub fn parse_local_subnets(ip_route_output: &str) -> Vec {
+ ip_route_output
+ .lines()
+ .filter(|l| !l.trim_start().starts_with("default"))
+ .filter(|l| !l.split_whitespace().any(|t| t == "lo"))
+ .filter_map(|l| l.split_whitespace().next())
+ .filter(|t| t.contains('/'))
+ .map(|s| s.to_string())
+ .collect()
+}
+
+/// Parst `nmap -sn`-Ausgabe: verknüpft "Nmap scan report for IP" mit der
+/// nächstfolgenden "MAC Address: XX:XX:XX:XX:XX:XX (Vendor)"-Zeile, bevor ein neuer
+/// Host-Block beginnt.
+pub fn parse_nmap_output(output: &str, mac: &MacAddress) -> Option {
+ let target = mac.to_lower_colon();
+ let mut current_ip: Option = None;
+
+ for line in output.lines() {
+ if let Some(rest) = line.strip_prefix("Nmap scan report for ") {
+ // rest ist entweder "192.168.1.5" oder "hostname (192.168.1.5)"
+ let rest = rest.trim();
+ current_ip = if let Some(open) = rest.rfind('(') {
+ rest[open + 1..].trim_end_matches(')').trim().parse().ok()
+ } else {
+ rest.parse().ok()
+ };
+ } else if let Some(rest) = line.trim_start().strip_prefix("MAC Address: ")
+ && let Some(mac_token) = rest.split_whitespace().next()
+ && mac_token.eq_ignore_ascii_case(&target)
+ && let Some(ip) = current_ip
+ {
+ return Some(ip);
+ }
+ }
+ None
+}
diff --git a/src/output.rs b/src/output.rs
new file mode 100644
index 0000000..522d960
--- /dev/null
+++ b/src/output.rs
@@ -0,0 +1,68 @@
+//! Human- und JSON-Ausgabe des Lookup-Ergebnisses.
+
+use serde::Serialize;
+
+use crate::error::Mac2IpError;
+use crate::mac::MacAddress;
+use crate::resolver::ResolveResult;
+
+#[derive(Serialize, Debug, PartialEq)]
+pub struct SuccessJson {
+ pub status: &'static str,
+ pub mac: String,
+ pub ip: String,
+ pub source: &'static str,
+}
+
+#[derive(Serialize, Debug, PartialEq)]
+pub struct FailureJson {
+ pub status: &'static str,
+ pub mac: String,
+ pub error: String,
+}
+
+pub fn success_json(result: &ResolveResult) -> SuccessJson {
+ SuccessJson {
+ status: "ok",
+ mac: result.mac.to_lower_colon(),
+ ip: result.ip.to_string(),
+ source: result.source.as_str(),
+ }
+}
+
+pub fn failure_json(mac: &MacAddress, err: &Mac2IpError) -> FailureJson {
+ FailureJson {
+ status: "error",
+ mac: mac.to_lower_colon(),
+ error: err.to_string(),
+ }
+}
+
+pub fn print_success(result: &ResolveResult, json: bool) {
+ if json {
+ println!(
+ "{}",
+ serde_json::to_string(&success_json(result))
+ .expect("JSON-Serialisierung schlägt nicht fehl")
+ );
+ } else {
+ println!(
+ "{} -> {} (Quelle: {})",
+ result.mac,
+ result.ip,
+ result.source.as_str()
+ );
+ }
+}
+
+pub fn print_failure(mac: &MacAddress, err: &Mac2IpError, json: bool) {
+ if json {
+ println!(
+ "{}",
+ serde_json::to_string(&failure_json(mac, err))
+ .expect("JSON-Serialisierung schlägt nicht fehl")
+ );
+ } else {
+ crate::log::error("mac2ip", &err.to_string());
+ }
+}
diff --git a/src/resolver.rs b/src/resolver.rs
new file mode 100644
index 0000000..1995611
--- /dev/null
+++ b/src/resolver.rs
@@ -0,0 +1,156 @@
+//! Orchestrierung des 3-Stufen-Algorithmus: Cache -> `ip neigh` -> `nmap`.
+
+use std::net::IpAddr;
+use std::time::{SystemTime, UNIX_EPOCH};
+
+use crate::cache::{self, Cache};
+use crate::config::AppConfig;
+use crate::error::Mac2IpError;
+use crate::mac::MacAddress;
+use crate::network::{self, NmapRunError, SudoMode};
+use crate::trust::{self, TrustDecision};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum MatchSource {
+ Cache,
+ Arp,
+ Nmap,
+}
+
+impl MatchSource {
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ Self::Cache => "cache",
+ Self::Arp => "arp",
+ Self::Nmap => "nmap",
+ }
+ }
+}
+
+pub struct ResolveResult {
+ pub mac: MacAddress,
+ pub ip: IpAddr,
+ pub source: MatchSource,
+}
+
+fn now_unix() -> i64 {
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|d| d.as_secs() as i64)
+ .unwrap_or(0)
+}
+
+pub async fn resolve(
+ mac: &MacAddress,
+ config: &AppConfig,
+ cache: Option<&Cache>,
+ json_mode: bool,
+ auto_trust_networks: bool,
+) -> Result {
+ let now = now_unix();
+
+ // Schritt 1: Cache
+ if let Some(cache) = cache
+ && let Some(entry) = cache.get(mac).await
+ && !cache::is_expired(entry.updated_at, now, config.cache_ttl_seconds)
+ && network::ping_check(entry.ip).await
+ {
+ crate::log::info(
+ "resolver",
+ &format!("Treffer im Cache für {mac}: {}", entry.ip),
+ );
+ return Ok(ResolveResult {
+ mac: *mac,
+ ip: entry.ip,
+ source: MatchSource::Cache,
+ });
+ }
+
+ // Schritt 2: ip neigh
+ crate::log::debug(
+ "resolver",
+ "Cache-Treffer nicht verfügbar, prüfe 'ip neigh show'",
+ );
+ if let Ok(output) = network::run_ip_neigh_show().await
+ && let Some(ip) = network::parse_ip_neigh_output(&output, mac)
+ && network::ping_check(ip).await
+ {
+ crate::log::info("resolver", &format!("Treffer via ip neigh für {mac}: {ip}"));
+ if let Some(cache) = cache {
+ cache.upsert(mac, ip, MatchSource::Arp.as_str(), now).await;
+ }
+ return Ok(ResolveResult {
+ mac: *mac,
+ ip,
+ source: MatchSource::Arp,
+ });
+ }
+
+ // Schritt 3: nmap
+ crate::log::debug("resolver", "Kein Treffer via ip neigh, starte nmap-Scan");
+
+ match trust::ensure_network_trusted(config, cache, auto_trust_networks, json_mode, now).await {
+ TrustDecision::Allowed => {}
+ TrustDecision::Denied { gateway_mac } => {
+ crate::log::warn(
+ "resolver",
+ "nmap-Scan übersprungen: Netzwerk nicht als vertrauenswürdig bestätigt",
+ );
+ return Err(Mac2IpError::UntrustedNetwork {
+ gateway_mac: gateway_mac.map(|m| m.to_string()),
+ });
+ }
+ }
+
+ let subnets = if !config.networks.is_empty() {
+ config.networks.clone()
+ } else {
+ network::run_ip_route_show_scope_link()
+ .await
+ .map(|o| network::parse_local_subnets(&o))
+ .unwrap_or_default()
+ };
+
+ let sudo_mode = if sudo_ctdra::is_run_as_root() {
+ SudoMode::None
+ } else if json_mode {
+ SudoMode::NonInteractive
+ } else {
+ SudoMode::Interactive
+ };
+
+ for cidr in subnets {
+ match network::run_nmap_scan(&cidr, config.nmap_timeout_seconds, sudo_mode).await {
+ Ok(output) => {
+ if let Some(ip) = network::parse_nmap_output(&output, mac)
+ && network::ping_check(ip).await
+ {
+ crate::log::info("resolver", &format!("Treffer via nmap für {mac}: {ip}"));
+ if let Some(cache) = cache {
+ cache.upsert(mac, ip, MatchSource::Nmap.as_str(), now).await;
+ }
+ return Ok(ResolveResult {
+ mac: *mac,
+ ip,
+ source: MatchSource::Nmap,
+ });
+ }
+ }
+ Err(NmapRunError::SudoUnavailable) => {
+ crate::log::warn(
+ "nmap",
+ "sudo -n fehlgeschlagen (keine Zugangsdaten) - Schritt 3 wird abgebrochen",
+ );
+ break; // sudo-Session-Status ist global, ein Retry pro Subnetz bringt nichts
+ }
+ Err(e) => {
+ crate::log::warn("nmap", &format!("nmap-Scan für {cidr} fehlgeschlagen: {e}"));
+ continue; // Fehler pro Subnetz (z. B. Timeout) - nächstes Subnetz versuchen
+ }
+ }
+ }
+
+ Err(Mac2IpError::NotFound {
+ mac: mac.to_string(),
+ })
+}
diff --git a/src/trust.rs b/src/trust.rs
new file mode 100644
index 0000000..7dd7075
--- /dev/null
+++ b/src/trust.rs
@@ -0,0 +1,110 @@
+//! Netzwerk-Vertrauensprüfung vor dem nmap-Scan (Schritt 3).
+//!
+//! Ein ARP-/Ping-Scan ganzer Subnetze ist auf Netzwerken, die man nicht selbst
+//! administriert (Firmen-/Gast-WLAN etc.), potenziell heikel (IDS-Alarm, Verstoß gegen
+//! Nutzungsordnungen). Das aktuelle Netzwerk wird daher über die MAC-Adresse seines
+//! Default-Gateways identifiziert (stabil über IP-/Subnetzwechsel hinweg) und nmap darf
+//! nur laufen, wenn dieses Netzwerk explizit vertraut ist:
+//! - statisch über `AppConfig::trusted_networks` (Config-Datei/CLI/ENV), oder
+//! - dynamisch über eine frühere Bestätigung, die im Cache gespeichert wurde, oder
+//! - automatisch, wenn `--auto-trust-networks` gesetzt ist (merkt sich das Netzwerk
+//! danach ebenfalls im Cache).
+//!
+//! Ist keins davon der Fall, wird im interaktiven Modus nachgefragt; im --json-Modus
+//! ist keine Rückfrage möglich, daher wird sicherheitshalber abgelehnt.
+
+use std::io::Write;
+
+use crate::cache::Cache;
+use crate::config::AppConfig;
+use crate::mac::MacAddress;
+use crate::network;
+
+pub enum TrustDecision {
+ Allowed,
+ Denied { gateway_mac: Option },
+}
+
+pub async fn ensure_network_trusted(
+ config: &AppConfig,
+ cache: Option<&Cache>,
+ auto_trust: bool,
+ json_mode: bool,
+ now: i64,
+) -> TrustDecision {
+ let Some(gateway_mac) = network::detect_default_gateway_mac().await else {
+ crate::log::warn(
+ "trust",
+ "Gateway-MAC konnte nicht ermittelt werden, Netzwerk kann nicht identifiziert werden",
+ );
+ if auto_trust {
+ crate::log::warn(
+ "trust",
+ "--auto-trust-networks gesetzt: nmap-Scan wird trotz unbekannter Gateway-MAC ausgeführt",
+ );
+ return TrustDecision::Allowed;
+ }
+ return TrustDecision::Denied { gateway_mac: None };
+ };
+
+ if config.trusted_network_macs().contains(&gateway_mac) {
+ return TrustDecision::Allowed;
+ }
+
+ if let Some(cache) = cache
+ && cache.is_trusted_network(&gateway_mac).await
+ {
+ return TrustDecision::Allowed;
+ }
+
+ if auto_trust {
+ crate::log::info(
+ "trust",
+ &format!(
+ "Netzwerk (Gateway-MAC {gateway_mac}) automatisch als vertrauenswürdig markiert"
+ ),
+ );
+ if let Some(cache) = cache {
+ cache.trust_network(&gateway_mac, now).await;
+ }
+ return TrustDecision::Allowed;
+ }
+
+ if json_mode {
+ // Keine Rückfrage im --json-Modus möglich (stdin ggf. nicht an ein Terminal
+ // gebunden, Ausgabe darf nicht durch einen Prompt verunreinigt werden).
+ return TrustDecision::Denied {
+ gateway_mac: Some(gateway_mac),
+ };
+ }
+
+ if prompt_trust_confirmation(gateway_mac).await {
+ if let Some(cache) = cache {
+ cache.trust_network(&gateway_mac, now).await;
+ }
+ TrustDecision::Allowed
+ } else {
+ TrustDecision::Denied {
+ gateway_mac: Some(gateway_mac),
+ }
+ }
+}
+
+/// Blockierende stdin-Abfrage, daher in `spawn_blocking` ausgelagert, damit der
+/// Tokio-Runtime-Thread währenddessen nicht blockiert.
+async fn prompt_trust_confirmation(gateway_mac: MacAddress) -> bool {
+ tokio::task::spawn_blocking(move || {
+ eprint!(
+ "Unbekanntes Netzwerk (Gateway-MAC {gateway_mac}). nmap-Scan in diesem Netzwerk erlauben und dauerhaft merken? [y/N]: "
+ );
+ let _ = std::io::stderr().flush();
+
+ let mut input = String::new();
+ if std::io::stdin().read_line(&mut input).is_err() {
+ return false;
+ }
+ matches!(input.trim().to_lowercase().as_str(), "y" | "yes" | "j" | "ja")
+ })
+ .await
+ .unwrap_or(false)
+}
diff --git a/tests/cache_ttl.rs b/tests/cache_ttl.rs
new file mode 100644
index 0000000..c13424d
--- /dev/null
+++ b/tests/cache_ttl.rs
@@ -0,0 +1,28 @@
+use mac2ip::cache::is_expired;
+
+#[test]
+fn fresh_entry_is_not_expired() {
+ assert!(!is_expired(1000, 1000, 1800));
+}
+
+#[test]
+fn entry_exactly_at_ttl_boundary_is_not_expired() {
+ // now - updated_at == ttl_seconds -> nicht abgelaufen (strikt größer nötig)
+ assert!(!is_expired(1000, 1000 + 1800, 1800));
+}
+
+#[test]
+fn entry_one_second_past_ttl_is_expired() {
+ assert!(is_expired(1000, 1000 + 1801, 1800));
+}
+
+#[test]
+fn zero_ttl_expires_immediately_after_any_elapsed_time() {
+ assert!(is_expired(1000, 1001, 0));
+ assert!(!is_expired(1000, 1000, 0));
+}
+
+#[test]
+fn large_gap_is_expired() {
+ assert!(is_expired(0, 1_000_000, 1800));
+}
diff --git a/tests/config_overrides.rs b/tests/config_overrides.rs
new file mode 100644
index 0000000..25a83ee
--- /dev/null
+++ b/tests/config_overrides.rs
@@ -0,0 +1,95 @@
+use std::path::PathBuf;
+
+use mac2ip::cli::Cli;
+use mac2ip::config::{AppConfig, apply_cli_overrides};
+use mac2ip::mac::MacAddress;
+
+fn base_cli() -> Cli {
+ Cli {
+ mac: MacAddress::parse("aa:bb:cc:dd:ee:ff").unwrap(),
+ json: false,
+ config: None,
+ log_level: None,
+ cache_ttl_seconds: None,
+ cache_db_path: None,
+ nmap_timeout_seconds: None,
+ networks: None,
+ trusted_networks: None,
+ auto_trust_networks: false,
+ }
+}
+
+#[test]
+fn no_overrides_leaves_config_untouched() {
+ let default_config = AppConfig::default();
+ let mut config = default_config.clone();
+ apply_cli_overrides(&mut config, &base_cli());
+ assert_eq!(config, default_config);
+}
+
+#[test]
+fn overrides_cache_ttl_seconds() {
+ let mut config = AppConfig::default();
+ let mut cli = base_cli();
+ cli.cache_ttl_seconds = Some(60);
+ apply_cli_overrides(&mut config, &cli);
+ assert_eq!(config.cache_ttl_seconds, 60);
+}
+
+#[test]
+fn overrides_cache_db_path() {
+ let mut config = AppConfig::default();
+ let mut cli = base_cli();
+ cli.cache_db_path = Some(PathBuf::from("/tmp/custom-cache.db"));
+ apply_cli_overrides(&mut config, &cli);
+ assert_eq!(config.cache_db_path, PathBuf::from("/tmp/custom-cache.db"));
+}
+
+#[test]
+fn overrides_nmap_timeout_seconds() {
+ let mut config = AppConfig::default();
+ let mut cli = base_cli();
+ cli.nmap_timeout_seconds = Some(30);
+ apply_cli_overrides(&mut config, &cli);
+ assert_eq!(config.nmap_timeout_seconds, 30);
+}
+
+#[test]
+fn overrides_networks() {
+ let mut config = AppConfig::default();
+ let mut cli = base_cli();
+ cli.networks = Some(vec!["10.0.0.0/24".to_string()]);
+ apply_cli_overrides(&mut config, &cli);
+ assert_eq!(config.networks, vec!["10.0.0.0/24".to_string()]);
+}
+
+#[test]
+fn overrides_trusted_networks() {
+ let mut config = AppConfig::default();
+ let mut cli = base_cli();
+ cli.trusted_networks = Some(vec![MacAddress::parse("aa:bb:cc:dd:ee:ff").unwrap()]);
+ apply_cli_overrides(&mut config, &cli);
+ assert_eq!(
+ config.trusted_networks,
+ vec!["aa:bb:cc:dd:ee:ff".to_string()]
+ );
+}
+
+#[test]
+fn partial_overrides_only_touch_provided_fields() {
+ let default_config = AppConfig::default();
+ let mut config = default_config.clone();
+ let mut cli = base_cli();
+ cli.cache_ttl_seconds = Some(999);
+ apply_cli_overrides(&mut config, &cli);
+
+ assert_eq!(config.cache_ttl_seconds, 999);
+ assert_eq!(config.cache_db_path, default_config.cache_db_path);
+ assert_eq!(config.log_level, default_config.log_level);
+ assert_eq!(
+ config.nmap_timeout_seconds,
+ default_config.nmap_timeout_seconds
+ );
+ assert_eq!(config.networks, default_config.networks);
+ assert_eq!(config.trusted_networks, default_config.trusted_networks);
+}
diff --git a/tests/gateway_trust.rs b/tests/gateway_trust.rs
new file mode 100644
index 0000000..b7ac6e0
--- /dev/null
+++ b/tests/gateway_trust.rs
@@ -0,0 +1,94 @@
+use std::net::IpAddr;
+
+use mac2ip::config::AppConfig;
+use mac2ip::mac::MacAddress;
+use mac2ip::network::{parse_default_gateway_ip, parse_ip_neigh_mac_for_ip};
+
+fn mac(s: &str) -> MacAddress {
+ MacAddress::parse(s).unwrap()
+}
+
+fn ip(s: &str) -> IpAddr {
+ s.parse().unwrap()
+}
+
+// --- parse_default_gateway_ip ---
+
+#[test]
+fn parses_gateway_ip_from_default_route() {
+ let output = "default via 192.168.1.1 dev eth0 proto dhcp metric 100\n";
+ assert_eq!(parse_default_gateway_ip(output), Some(ip("192.168.1.1")));
+}
+
+#[test]
+fn parses_gateway_ip_ignoring_extra_trailing_fields() {
+ let output = "default via 10.0.0.254 dev wlan0 proto static metric 600 onlink\n";
+ assert_eq!(parse_default_gateway_ip(output), Some(ip("10.0.0.254")));
+}
+
+#[test]
+fn no_default_route_returns_none() {
+ assert!(parse_default_gateway_ip("").is_none());
+}
+
+#[test]
+fn missing_via_token_returns_none() {
+ let output = "default dev eth0 proto dhcp metric 100\n";
+ assert!(parse_default_gateway_ip(output).is_none());
+}
+
+// --- parse_ip_neigh_mac_for_ip ---
+
+#[test]
+fn finds_mac_for_matching_ip() {
+ let output = "192.168.1.1 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE\n";
+ let result = parse_ip_neigh_mac_for_ip(output, ip("192.168.1.1"));
+ assert_eq!(result, Some(mac("aa:bb:cc:dd:ee:ff")));
+}
+
+#[test]
+fn ignores_entries_for_other_ips() {
+ let output = "\
+192.168.1.2 dev eth0 lladdr 11:22:33:44:55:66 STALE
+192.168.1.1 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE
+";
+ let result = parse_ip_neigh_mac_for_ip(output, ip("192.168.1.1"));
+ assert_eq!(result, Some(mac("aa:bb:cc:dd:ee:ff")));
+}
+
+#[test]
+fn returns_none_when_ip_not_present() {
+ let output = "192.168.1.2 dev eth0 lladdr 11:22:33:44:55:66 STALE\n";
+ assert!(parse_ip_neigh_mac_for_ip(output, ip("192.168.1.1")).is_none());
+}
+
+// --- AppConfig::trusted_network_macs ---
+
+#[test]
+fn trusted_network_macs_parses_valid_entries() {
+ let config = AppConfig {
+ trusted_networks: vec!["aa:bb:cc:dd:ee:ff".to_string()],
+ ..Default::default()
+ };
+ assert_eq!(
+ config.trusted_network_macs(),
+ vec![mac("aa:bb:cc:dd:ee:ff")]
+ );
+}
+
+#[test]
+fn trusted_network_macs_skips_invalid_entries() {
+ let config = AppConfig {
+ trusted_networks: vec!["not-a-mac".to_string(), "aa:bb:cc:dd:ee:ff".to_string()],
+ ..Default::default()
+ };
+ assert_eq!(
+ config.trusted_network_macs(),
+ vec![mac("aa:bb:cc:dd:ee:ff")]
+ );
+}
+
+#[test]
+fn trusted_network_macs_empty_by_default() {
+ assert!(AppConfig::default().trusted_network_macs().is_empty());
+}
diff --git a/tests/ip_neigh_parsing.rs b/tests/ip_neigh_parsing.rs
new file mode 100644
index 0000000..55d50ae
--- /dev/null
+++ b/tests/ip_neigh_parsing.rs
@@ -0,0 +1,52 @@
+use mac2ip::mac::MacAddress;
+use mac2ip::network::parse_ip_neigh_output;
+
+fn mac(s: &str) -> MacAddress {
+ MacAddress::parse(s).unwrap()
+}
+
+#[test]
+fn finds_matching_entry() {
+ let output = "192.168.1.5 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE\n";
+ let ip = parse_ip_neigh_output(output, &mac("aa:bb:cc:dd:ee:ff")).unwrap();
+ assert_eq!(ip.to_string(), "192.168.1.5");
+}
+
+#[test]
+fn picks_correct_entry_among_multiple() {
+ let output = "\
+192.168.1.4 dev eth0 lladdr 11:22:33:44:55:66 STALE
+192.168.1.5 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE
+192.168.1.6 dev eth0 lladdr 77:88:99:aa:bb:cc STALE
+";
+ let ip = parse_ip_neigh_output(output, &mac("aa:bb:cc:dd:ee:ff")).unwrap();
+ assert_eq!(ip.to_string(), "192.168.1.5");
+}
+
+#[test]
+fn skips_entries_without_lladdr() {
+ let output = "\
+192.168.1.7 dev eth0 FAILED
+192.168.1.5 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE
+";
+ let ip = parse_ip_neigh_output(output, &mac("aa:bb:cc:dd:ee:ff")).unwrap();
+ assert_eq!(ip.to_string(), "192.168.1.5");
+}
+
+#[test]
+fn returns_none_when_no_match() {
+ let output = "192.168.1.4 dev eth0 lladdr 11:22:33:44:55:66 STALE\n";
+ assert!(parse_ip_neigh_output(output, &mac("aa:bb:cc:dd:ee:ff")).is_none());
+}
+
+#[test]
+fn tolerates_blank_lines_and_extra_whitespace() {
+ let output = "\n \n192.168.1.5 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE\n\n";
+ let ip = parse_ip_neigh_output(output, &mac("aa:bb:cc:dd:ee:ff")).unwrap();
+ assert_eq!(ip.to_string(), "192.168.1.5");
+}
+
+#[test]
+fn empty_output_returns_none() {
+ assert!(parse_ip_neigh_output("", &mac("aa:bb:cc:dd:ee:ff")).is_none());
+}
diff --git a/tests/json_output.rs b/tests/json_output.rs
new file mode 100644
index 0000000..d12441b
--- /dev/null
+++ b/tests/json_output.rs
@@ -0,0 +1,90 @@
+use std::net::IpAddr;
+use std::str::FromStr;
+
+use mac2ip::error::Mac2IpError;
+use mac2ip::mac::MacAddress;
+use mac2ip::output::{SuccessJson, failure_json, success_json};
+use mac2ip::resolver::{MatchSource, ResolveResult};
+
+fn mac() -> MacAddress {
+ MacAddress::parse("aa:bb:cc:dd:ee:ff").unwrap()
+}
+
+fn ip() -> IpAddr {
+ IpAddr::from_str("192.168.1.5").unwrap()
+}
+
+#[test]
+fn success_json_has_expected_shape_for_cache_source() {
+ let result = ResolveResult {
+ mac: mac(),
+ ip: ip(),
+ source: MatchSource::Cache,
+ };
+ let json = success_json(&result);
+ assert_eq!(
+ json,
+ SuccessJson {
+ status: "ok",
+ mac: "aa:bb:cc:dd:ee:ff".to_string(),
+ ip: "192.168.1.5".to_string(),
+ source: "cache",
+ }
+ );
+}
+
+#[test]
+fn success_json_has_expected_shape_for_arp_source() {
+ let result = ResolveResult {
+ mac: mac(),
+ ip: ip(),
+ source: MatchSource::Arp,
+ };
+ assert_eq!(success_json(&result).source, "arp");
+}
+
+#[test]
+fn success_json_has_expected_shape_for_nmap_source() {
+ let result = ResolveResult {
+ mac: mac(),
+ ip: ip(),
+ source: MatchSource::Nmap,
+ };
+ assert_eq!(success_json(&result).source, "nmap");
+}
+
+#[test]
+fn failure_json_has_expected_shape() {
+ let err = Mac2IpError::NotFound {
+ mac: mac().to_string(),
+ };
+ let json = failure_json(&mac(), &err);
+ assert_eq!(json.status, "error");
+ assert_eq!(json.mac, "aa:bb:cc:dd:ee:ff");
+ assert_eq!(json.error, err.to_string());
+}
+
+#[test]
+fn success_json_serializes_with_stable_field_names() {
+ let result = ResolveResult {
+ mac: mac(),
+ ip: ip(),
+ source: MatchSource::Arp,
+ };
+ let value = serde_json::to_value(success_json(&result)).unwrap();
+ assert_eq!(value["status"], "ok");
+ assert_eq!(value["mac"], "aa:bb:cc:dd:ee:ff");
+ assert_eq!(value["ip"], "192.168.1.5");
+ assert_eq!(value["source"], "arp");
+}
+
+#[test]
+fn failure_json_serializes_with_stable_field_names() {
+ let err = Mac2IpError::NotFound {
+ mac: mac().to_string(),
+ };
+ let value = serde_json::to_value(failure_json(&mac(), &err)).unwrap();
+ assert_eq!(value["status"], "error");
+ assert_eq!(value["mac"], "aa:bb:cc:dd:ee:ff");
+ assert!(value["error"].is_string());
+}
diff --git a/tests/mac_address.rs b/tests/mac_address.rs
new file mode 100644
index 0000000..f26c534
--- /dev/null
+++ b/tests/mac_address.rs
@@ -0,0 +1,65 @@
+use mac2ip::mac::MacAddress;
+
+#[test]
+fn parses_lowercase_colon_separated() {
+ let mac = MacAddress::parse("aa:bb:cc:dd:ee:ff").unwrap();
+ assert_eq!(mac.to_lower_colon(), "aa:bb:cc:dd:ee:ff");
+}
+
+#[test]
+fn parses_uppercase_colon_separated() {
+ let mac = MacAddress::parse("AA:BB:CC:DD:EE:FF").unwrap();
+ assert_eq!(mac.to_lower_colon(), "aa:bb:cc:dd:ee:ff");
+ assert_eq!(mac.to_upper_colon(), "AA:BB:CC:DD:EE:FF");
+}
+
+#[test]
+fn parses_hyphen_separated() {
+ let mac = MacAddress::parse("aa-bb-cc-dd-ee-ff").unwrap();
+ assert_eq!(mac.to_lower_colon(), "aa:bb:cc:dd:ee:ff");
+}
+
+#[test]
+fn parses_mixed_case() {
+ let mac = MacAddress::parse("Aa:bB:Cc:dD:eE:fF").unwrap();
+ assert_eq!(mac.to_lower_colon(), "aa:bb:cc:dd:ee:ff");
+}
+
+#[test]
+fn different_notations_are_equal_after_canonicalization() {
+ let a = MacAddress::parse("AA:BB:CC:DD:EE:FF").unwrap();
+ let b = MacAddress::parse("aa-bb-cc-dd-ee-ff").unwrap();
+ assert_eq!(a, b);
+}
+
+#[test]
+fn from_str_matches_parse() {
+ use std::str::FromStr;
+ let mac: MacAddress = "aa:bb:cc:dd:ee:ff".parse().unwrap();
+ assert_eq!(mac, MacAddress::from_str("aa:bb:cc:dd:ee:ff").unwrap());
+}
+
+#[test]
+fn rejects_too_short() {
+ assert!(MacAddress::parse("aa:bb:cc:dd:ee").is_err());
+}
+
+#[test]
+fn rejects_too_long() {
+ assert!(MacAddress::parse("aa:bb:cc:dd:ee:ff:00").is_err());
+}
+
+#[test]
+fn rejects_non_hex_characters() {
+ assert!(MacAddress::parse("gg:bb:cc:dd:ee:ff").is_err());
+}
+
+#[test]
+fn rejects_wrong_separator_count() {
+ assert!(MacAddress::parse("aabbccddeeff").is_err());
+}
+
+#[test]
+fn rejects_empty_string() {
+ assert!(MacAddress::parse("").is_err());
+}
diff --git a/tests/nmap_parsing.rs b/tests/nmap_parsing.rs
new file mode 100644
index 0000000..4f5ec73
--- /dev/null
+++ b/tests/nmap_parsing.rs
@@ -0,0 +1,74 @@
+use mac2ip::mac::MacAddress;
+use mac2ip::network::parse_nmap_output;
+
+fn mac(s: &str) -> MacAddress {
+ MacAddress::parse(s).unwrap()
+}
+
+#[test]
+fn finds_matching_host_among_multiple_blocks() {
+ let output = "\
+Nmap scan report for 192.168.1.4
+Host is up (0.0010s latency).
+MAC Address: 11:22:33:44:55:66 (Some Vendor)
+Nmap scan report for 192.168.1.5
+Host is up (0.0020s latency).
+MAC Address: AA:BB:CC:DD:EE:FF (Other Vendor)
+Nmap scan report for 192.168.1.6
+Host is up (0.0030s latency).
+MAC Address: 77:88:99:AA:BB:CC (Third Vendor)
+";
+ let ip = parse_nmap_output(output, &mac("aa:bb:cc:dd:ee:ff")).unwrap();
+ assert_eq!(ip.to_string(), "192.168.1.5");
+}
+
+#[test]
+fn host_without_mac_line_does_not_leak_into_next_block() {
+ let output = "\
+Nmap scan report for 192.168.1.4
+Host is up (0.0010s latency).
+Nmap scan report for 192.168.1.5
+Host is up (0.0020s latency).
+MAC Address: AA:BB:CC:DD:EE:FF (Vendor)
+";
+ // Even if we searched for a MAC that never appears, the missing MAC line for
+ // 192.168.1.4 must not accidentally get associated with 192.168.1.5's MAC.
+ assert!(parse_nmap_output(output, &mac("11:22:33:44:55:66")).is_none());
+ let ip = parse_nmap_output(output, &mac("aa:bb:cc:dd:ee:ff")).unwrap();
+ assert_eq!(ip.to_string(), "192.168.1.5");
+}
+
+#[test]
+fn parses_hostname_plus_ip_form() {
+ let output = "\
+Nmap scan report for myhost.lan (192.168.1.5)
+Host is up (0.0020s latency).
+MAC Address: AA:BB:CC:DD:EE:FF (Vendor)
+";
+ let ip = parse_nmap_output(output, &mac("aa:bb:cc:dd:ee:ff")).unwrap();
+ assert_eq!(ip.to_string(), "192.168.1.5");
+}
+
+#[test]
+fn is_case_insensitive_against_uppercase_nmap_output() {
+ let output = "\
+Nmap scan report for 192.168.1.5
+MAC Address: AA:BB:CC:DD:EE:FF (Vendor)
+";
+ let ip = parse_nmap_output(output, &mac("aa:bb:cc:dd:ee:ff")).unwrap();
+ assert_eq!(ip.to_string(), "192.168.1.5");
+}
+
+#[test]
+fn returns_none_when_no_match() {
+ let output = "\
+Nmap scan report for 192.168.1.4
+MAC Address: 11:22:33:44:55:66 (Vendor)
+";
+ assert!(parse_nmap_output(output, &mac("aa:bb:cc:dd:ee:ff")).is_none());
+}
+
+#[test]
+fn empty_output_returns_none() {
+ assert!(parse_nmap_output("", &mac("aa:bb:cc:dd:ee:ff")).is_none());
+}
diff --git a/tests/subnet_parsing.rs b/tests/subnet_parsing.rs
new file mode 100644
index 0000000..eeaea2a
--- /dev/null
+++ b/tests/subnet_parsing.rs
@@ -0,0 +1,40 @@
+use mac2ip::network::parse_local_subnets;
+
+#[test]
+fn extracts_cidrs_from_multiple_interfaces() {
+ let output = "\
+192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.23
+10.0.0.0/8 dev wlan0 proto kernel scope link src 10.0.0.5
+";
+ let subnets = parse_local_subnets(output);
+ assert_eq!(
+ subnets,
+ vec!["192.168.1.0/24".to_string(), "10.0.0.0/8".to_string()]
+ );
+}
+
+#[test]
+fn excludes_default_route() {
+ let output = "\
+default via 192.168.1.1 dev eth0
+192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.23
+";
+ let subnets = parse_local_subnets(output);
+ assert_eq!(subnets, vec!["192.168.1.0/24".to_string()]);
+}
+
+#[test]
+fn excludes_loopback_interface() {
+ let output = "\
+127.0.0.0/8 dev lo proto kernel scope link src 127.0.0.1
+192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.23
+";
+ let subnets = parse_local_subnets(output);
+ assert_eq!(subnets, vec!["192.168.1.0/24".to_string()]);
+}
+
+#[test]
+fn empty_input_returns_empty_vec() {
+ let subnets = parse_local_subnets("");
+ assert!(subnets.is_empty());
+}