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). +[![Main Release & Publish](https://gitea.creative-dragonslayer.de/Linuxapps/Mac2Ip/actions/workflows/main.yaml/badge.svg?branch=main)](https://gitea.creative-dragonslayer.de/Linuxapps/Mac2Ip/actions?workflow=main.yaml) +[![Testing Build, Publish & Preview Release](https://gitea.creative-dragonslayer.de/Linuxapps/Mac2Ip/actions/workflows/testing.yaml/badge.svg?branch=testing)](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()); +}