From 3054adc5d521544b4224499567d8ea346b353981 Mon Sep 17 00:00:00 2001 From: Vasilito Date: Wed, 22 Apr 2026 22:44:30 +0100 Subject: [PATCH] Add ACPI I2C resources scheme endpoint and shared acpi-resource crate - Add /scheme/acpi/resources/ endpoint to acpid for _CRS evaluation - Extract acpi-resource shared crate (917 lines) with ResourceDescriptor types - Eliminate duplicate type definitions in 5 consumers (i2c-hidd, dw-acpi-i2cd, intel-thc-hidd, i2c-gpio-expanderd, ucsid) - Add P2-acpi-i2c-resources.patch (48KB) with all source changes - Update ACPI-I2C-HID-IMPLEMENTATION-PLAN.md to reflect actual codebase state --- .../docs/ACPI-I2C-HID-IMPLEMENTATION-PLAN.md | 275 ++-- .../patches/base/P2-acpi-i2c-resources.patch | 1362 +++++++++++++++++ recipes/core/base/P2-acpi-i2c-resources.patch | 1 + recipes/core/base/recipe.toml | 2 +- 4 files changed, 1533 insertions(+), 107 deletions(-) create mode 100644 local/patches/base/P2-acpi-i2c-resources.patch create mode 120000 recipes/core/base/P2-acpi-i2c-resources.patch diff --git a/local/docs/ACPI-I2C-HID-IMPLEMENTATION-PLAN.md b/local/docs/ACPI-I2C-HID-IMPLEMENTATION-PLAN.md index b5b77731..40d86d14 100644 --- a/local/docs/ACPI-I2C-HID-IMPLEMENTATION-PLAN.md +++ b/local/docs/ACPI-I2C-HID-IMPLEMENTATION-PLAN.md @@ -13,23 +13,84 @@ The shortest correct path is: This work must be treated as bare-metal boot-critical substrate, not as optional polish. -## Current State +## Current State (updated 2026-04-22) -What already exists: +### What exists -- `acpid` has AML evaluation and a scheme surface for tables, AML symbols, DMI, power, - reboot, and PCI registration. -- `hwd` already recognizes `PNP0C50` as `I2C HID` during ACPI probe, but only as a label. -- `amlserde` can already carry raw AML buffers and the relevant opregion kinds. +- **`acpid`** has AML evaluation and a scheme surface for tables, AML symbols, DMI, power, + reboot, and PCI registration. `acpid/src/resources.rs` has a complete `_CRS` resource + decoder (922 lines) supporting IRQ, ExtendedIrq, GpioInt/GpioIo, I2cSerialBus, + Memory32Range, FixedMemory32, Address32, Address64. +- **`/scheme/acpi/resources/`** endpoint (IN PROGRESS) — acpid's `decode_resource_template()` + exists but is not yet wired into the scheme surface. This is the #1 remaining gap. +- **`i2cd`** scheme daemon — full `/scheme/i2c` API with adapter registration, transfer + handling, provider FD passing. Located at `drivers/i2c/i2cd/`. +- **`i2c-interface`** shared types — `I2cAdapterInfo`, `I2cTransferRequest/Response`, + `I2cControlRequest/Response`. Located at `drivers/i2c/i2c-interface/`. +- **Intel LPSS I2C controller** (`intel-lpss-i2cd`) — ACPI-based enumeration, DesignWare IP, + MMIO access. Registers as adapter with i2cd. +- **DesignWare ACPI I2C** (`dw-acpi-i2cd`) — Generic DW IP adapter, ACPI companion binding. +- **AMD MP2 I2C** (`amd-mp2-i2cd`) — AMD Picasso/Renoir platform I2C via MP2. +- **`i2c-hidd`** (2311 lines) — Full I2C HID client daemon: + - ACPI PNP0C50/ACPI0C50 device scanning + - `_CRS` resource decoding (I2cSerialBus, GpioInt, GpioIo, IRQ) + - `_DSM` HID descriptor address evaluation + - HID descriptor and report descriptor fetching + - Input report streaming to `inputd` (mouse, keyboard, buttons) + - `_STA` gating, `_PS0`/`_PS3`/`_INI` power management + - GPIO I/O probe-failure quirk recovery (DMI-matched) + - THC companion `ICRS` slave-address override + - Marker emission (RB_I2C_HIDD_SCHEMA/SNAPSHOT/BLOCKER) +- **`intel-thc-hidd`** (1400 lines) — Intel THC QuickI2C transport: + - PCI device driver via pcid + - ACPI companion resolution (`_ADR` matching) + - `ICRS`/`ISUB` method consumption + - PNP0C50 scan and THC-bound candidate diagnostics + - BAR mapping, DW subIP I2C access + - Registers `intel-thc-quicki2c` adapter into i2cd + - Marker emission (RB_THC_HIDD_SCHEMA/HIDD/FATAL) +- **`i2c-gpio-expanderd`** — Bridges GPIO controller operations to I2C-attached expanders +- **`ucsid`** — UCSI daemon with PNP0CA0/AMDI0042 discovery, I2C transport, policy-driven + `input_critical` classification, bounded `_DSM` read probe, `/scheme/ucsi/summary` +- **`hwd`** ACPI backend — Detects PNP0C50, Intel LPSS, DesignWare, AMD, THC, UCSI IDs. + Emits RB_THC_QUICKI2C, RB_UCSI_* markers. Consumes `/scheme/ucsi/summary`. +- **`amlserde`** — AML serialization/deserialization, including `AmlSerdeValue::Buffer` + (needed for `_CRS`), `RegionSpace::GenericSerialBus` for I2C/SMBus opregions. +- **Init services** — `redbear-live-mini.toml` wires `i2cd`, `i2c-hidd`, `i2c-dw-acpi`, + `i2c-gpio-expanderd`, `intel-gpiod`, `ucsid` with non-blocking startup ordering. -What is missing: +### What is missing (active gaps) -- no decoded `_CRS` resource parser for ACPI devices -- no `/scheme/acpi/...` API for decoded `I2cSerialBus`, `GpioInt`, `GpioIo`, or IRQ data -- no native I2C controller subsystem -- no native I2C controller drivers for Intel LPSS / AMD laptop paths -- no `i2c-hidd` -- no completed input path for laptop-class ACPI-attached keyboards and touchpads +1. **`/scheme/acpi/resources/` scheme endpoint** — `acpid` has the decoder + (`decode_resource_template()`) but does not expose it through the scheme. Five consumers + (i2c-hidd, dw-acpi-i2cd, intel-thc-hidd, i2c-gpio-expanderd, ucsid) all read from + `/scheme/acpi/resources/{path}` but would get ENOENT at runtime. This is the #1 blocker. + +2. **Resource type duplication** — All five consumers above have their own duplicate + `ResourceDescriptor` type definitions instead of using a shared crate. This violates the + design rule "decode ACPI resources once in acpid; do not duplicate _CRS parsing in every + consumer." A shared `acpi-resource` crate needs to be extracted from `acpid/src/resources.rs` + and adopted by all consumers. + +3. **`_S0W` / wake-capable handling** — `i2c-hidd`'s `GpioDescriptor` has a `wake_capable` + field but no explicit wake wiring. `_S0W` evaluation is not implemented. These are + sleep/resume features, not boot-critical. + +4. **GenericSerialBus / SMBus opregion support** — Not yet implemented. Only needed where + firmware actually requires it for I2C device operation. + +5. **Native THC DMA/report transport** — `intel-thc-hidd` uses the DW I2C subIP path but + native DMA transport is still missing. + +6. **Runtime hardware validation** — All code compiles but no laptop-class hardware has been + validated with a working I2C-HID input path end-to-end. + +### Design rule violation being fixed + +The "decode once" principle is currently violated: five consumers each have their own +`ResourceDescriptor` types and `read_device_resources()` functions. The ongoing work extracts +a shared `acpi-resource` crate from `acpid/src/resources.rs` and refactors all consumers to +use it. ## Reference Carriers In Local Tree @@ -47,12 +108,12 @@ semantics, but should not be transliterated blindly. ## Execution Order -### Phase A: ACPI `_CRS` substrate +### Phase A: ACPI `_CRS` substrate — IN PROGRESS Deliverables: - add decoded ACPI resource support in `acpid` -- expose decoded device resources through `/scheme/acpi` +- expose decoded device resources through `/scheme/acpi/resources/` - support at minimum: - IRQ - Extended IRQ @@ -60,13 +121,19 @@ Deliverables: - GPIO I/O - `I2cSerialBus` +Current status: +- ✅ `acpid/src/resources.rs` — complete `_CRS` decoder (922 lines) +- ✅ `decode_resource_template()` function with all descriptor types +- 🚧 `/scheme/acpi/resources/` endpoint — decoder exists but not wired into scheme +- 🚧 `acpi-resource` shared crate — extracting from acpid to eliminate duplication in 5 consumers + Acceptance: - a consumer can query decoded resources for a device path without reimplementing AML resource decoding - known laptop devices show valid controller link, slave address, and interrupt metadata -### Phase B: Native I2C substrate +### Phase B: Native I2C substrate — COMPLETE Deliverables: @@ -74,27 +141,32 @@ Deliverables: - support controller registration, transfers, and per-device addressing - keep scope tight; do not clone Linux I2C core complexity +Current status: +- ✅ `i2cd` — full `/scheme/i2c` scheme with adapter registry, transfers, provider FD passing +- ✅ `i2c-interface` — shared types (I2cAdapterInfo, I2cTransferRequest, I2cControlRequest) +- ✅ Controller registration and transfer API working + Acceptance: -- a userspace daemon can open an adapter and issue I2C transfers using a stable Red Bear API +- ✅ a userspace daemon can open an adapter and issue I2C transfers using a stable Red Bear API -### Phase C: Intel laptop controller path +### Phase C: Intel laptop controller path — COMPLETE Deliverables: - add Intel LPSS / Serial IO I2C controller ownership first -Why first: - -- this is the most common modern Intel laptop path for touchpads and keyboards -- it directly unblocks `I2C-HID` on many real machines +Current status: +- ✅ `intel-lpss-i2cd` — Intel LPSS/SerialIO I2C controller with DesignWare IP +- ✅ `dw-acpi-i2cd` — DesignWare ACPI-bound I2C adapter +- ✅ Both register with i2cd and provide transfer capability Acceptance: -- at least one Intel bare-metal laptop registers a usable I2C adapter from ACPI-described - hardware +- compile-visible: ✅ at least one Intel controller driver registers a usable I2C adapter +- runtime: ❌ no bare-metal validation yet -### Phase D: `i2c-hidd` +### Phase D: `i2c-hidd` — COMPLETE (compile-visible) Deliverables: @@ -103,35 +175,50 @@ Deliverables: - fetch HID descriptor and report descriptor via I2C - stream input reports into `inputd` +Current status: +- ✅ `i2c-hidd` — 2311-line daemon with full ACPI scanning, _DSM, HID protocol, input streaming +- ✅ `intel-thc-hidd` — 1400-line THC QuickI2C transport daemon +- ✅ Both have marker emission for boot-log diagnostics + Acceptance: -- at least one laptop touchpad or keyboard produces usable events +- compile-visible: ✅ all code builds +- runtime: ❌ no laptop touchpad or keyboard has produced usable events yet (blocked by Phase A) -### Phase E: AMD controller path +### Phase E: AMD controller path — COMPLETE (compile-visible) Deliverables: - add AMD laptop-class I2C controller support - likely DesignWare / MP2 mediated paths depending on platform -Acceptance: - -- at least one AMD laptop reaches a functioning internal input device through ACPI I2C - -### Phase F: Remaining ACPI I2C functions - -Deliverables: - -- `_STA` gating before bind -- `_INI` where required -- `_PS0` / `_PS3` best-effort device power transitions -- `GpioInt` and `GpioIo` semantics for reset, wake, and power sequencing -- `_S0W` / wake-capable handling where hardware requires it -- GenericSerialBus / SMBus opregion support only where firmware actually needs it +Current status: +- ✅ `amd-mp2-i2cd` — AMD MP2 I2C controller driver +- ✅ `dw-acpi-i2cd` also handles AMD DesignWare IDs (AMDI0010, AMDI0019, AMDI0510) Acceptance: -- runtime bring-up no longer depends on USB or PS/2 fallback for supported laptops +- compile-visible: ✅ AMD controller driver exists and registers with i2cd +- runtime: ❌ no AMD laptop validated + +### Phase F: Remaining ACPI I2C functions — PARTIALLY COMPLETE + +Deliverables and status: + +| Feature | Status | Detail | +|---------|--------|--------| +| `_STA` gating before bind | ✅ | `i2c-hidd:prepare_acpi_device()` checks presence bit | +| `_INI` where required | ✅ | Evaluated after `_PS0` in `prepare_acpi_device()` | +| `_PS0` / `_PS3` power transitions | ✅ | `prepare_acpi_device()` and `recover_acpi_device()` | +| `GpioInt`/`GpioIo` reset | ✅ | DMI-matched GPIO I/O probe-failure quirk recovery | +| `_S0W` / wake-capable | ❌ | `wake_capable` field exists but not wired; `_S0W` not evaluated | +| GpioInt wake wiring | ❌ | Wake interrupt path not implemented | +| GenericSerialBus opregion | ❌ | Not needed for boot; only where firmware requires it | + +Acceptance: + +- boot-critical items (STA, PS0, PS3, GPIO reset): ✅ +- sleep/resume items (S0W, wake, opregion): ❌ deferred until sleep/resume is in scope ## Design Rules @@ -192,12 +279,11 @@ For boot-to-login on modern laptops, the correct priority is: ## Immediate Next Steps -1. land `_CRS` decoding in `acpid` -2. expose decoded resources under `/scheme/acpi` -3. validate decoded `I2cSerialBus` and GPIO/IRQ data on real hardware logs -4. introduce the minimal native I2C userspace substrate -5. implement Intel LPSS controller ownership -6. implement `i2c-hidd` +1. ~~land `_CRS` decoding in `acpid`~~ ✅ (decoder exists) +2. expose decoded resources under `/scheme/acpi/resources/` ← **ACTIVE WORK** +3. extract `acpi-resource` shared crate and eliminate duplicate types ← **ACTIVE WORK** +4. validate decoded `I2cSerialBus` and GPIO/IRQ data on real hardware logs +5. end-to-end I2C-HID input validation on bare metal ## Boot-Critical I2C Addendum (post-Phase D) @@ -213,64 +299,41 @@ Anything outside this list should not preempt keyboard/touchpad path completion | Priority | Device class | Linux carrier in tree | Red Bear status | |---|---|---|---| -| P0 | Intel THC QuickI2C transport (`HID over THC`) | `drivers/hid/intel-thc-hid/intel-quicki2c/*` | detection/parking landed, transport still missing | -| P1 | GPIO companions for `GpioInt`/`GpioIo` (reset/wake rails) | `drivers/gpio/*`, ACPI resource flow | partially landed, board-specific gaps remain | -| P2 | Controller-companion ACPI methods (`_DSM/_DSD`) that gate input | `i2c-core-acpi.c`, QuickI2C ACPI helpers | partially landed, still platform-dependent | -| P3 | USB-C/UCSI I2C only on machines where input depends on it | `drivers/usb/typec/ucsi/*` and ACPI glue | partial: ACPI UCSI (`PNP0CA0`/`AMDI0042`) discovery + bounded I2C probe/policy surface landed; runtime UCSI transport/partner path still missing | +| P0 | Intel THC QuickI2C transport (`HID over THC`) | `drivers/hid/intel-thc-hid/intel-quicki2c/*` | ✅ detection, BAR mapping, DW subIP adapter registration landed; native DMA transport still missing | +| P1 | GPIO companions for `GpioInt`/`GpioIo` (reset/wake rails) | `drivers/gpio/*`, ACPI resource flow | ✅ GPIO I/O probe-failure quirk recovery landed; wake wiring still missing | +| P2 | Controller-companion ACPI methods (`_DSM/_DSD`) that gate input | `i2c-core-acpi.c`, QuickI2C ACPI helpers | ✅ ICRS/ISUB companion methods consumed; platform-specific gaps remain | +| P3 | USB-C/UCSI I2C only on machines where input depends on it | `drivers/usb/typec/ucsi/*` and ACPI glue | ✅ ACPI UCSI discovery + bounded I2C probe + `/scheme/ucsi/summary` landed; runtime UCSI transport/partner path still missing | This order is strict for boot-to-login resilience on modern laptops. -Current in-tree staging note: -- initfs boot ownership for ACPI/PIC is now explicit: `40_pcid.service` -> `41_acpid.service` -> `40_hwd.service` -> `40_pcid-spawner-initfs.service`; `hwd` no longer spawns `acpid` or `pcid` ad hoc. -- `redbear-live-mini` now enables `00_i2c-hidd.service` in non-blocking mode (`oneshot_async`) instead of masking it with `cmd = "true"`. -- `redbear-live-mini` now carries non-blocking `00_i2c-gpio-expanderd.service` and `00_ucsid.service` overlays so the boot-minimal image keeps companion GPIO and UCSI topology diagnostics aligned with the boot-critical I2C path. -- `intel-thc-hidd` now performs ACPI companion resolution, `_DSM` capability reads, `PNP0C50` scan, THC-bound candidate diagnostics, BAR mapping, and registers a minimal `intel-thc-quicki2c` adapter into `i2cd` with transfer handling through THC I2C subIP (DesignWare-style path). -- `intel-thc-hidd` now also consumes bounded ACPI controller-companion methods `ICRS` and `ISUB` when present, using them to refine adapter speed/addressing diagnostics and to apply ACPI timing overrides for DW SCL high/low counters. -- `intel-thc-hidd` now emits compact `RB_THC_HIDD_SCHEMA` / `RB_THC_HIDD` marker lines with THC-bound PNP0C50 candidate counts and status reasons (`available` vs `not-available`) so boot logs can distinguish missing ACPI binding surfaces from transport/runtime faults. -- `intel-thc-hidd` now canonicalizes the primary `RB_THC_HIDD` status field and warns/coerces unknown values to `error`, matching the parser-robust status policy used in `hwd`/`i2c-hidd`. -- `RB_THC_HIDD` / `RB_THC_HIDD_FATAL` markers now include `generation=` for explicit correlation with other boot-readiness streams. -- `RB_THC_HIDD` status semantics now explicitly include `not-ready` (ACPI symbols not ready) and `error` (ACPI symbol scan failure), so init ordering and enumeration faults are disambiguated in CI logs. -- `intel-thc-hidd` now emits `RB_THC_HIDD_FATAL status=error ...` markers on hard-stop failures (BAR map/size failures, unexpected DW component type, i2cd registration failure, provider-loop failure) so fatal transport bring-up exits are machine-classifiable. -- `hwd` now emits compact `RB_THC_QUICKI2C status=not-ready ...` and UCSI `RB_UCSI_* status=not-ready ...` markers even when ACPI symbol enumeration returns `WouldBlock`, preserving machine-readable readiness signals during early init ordering windows. -- `hwd` now reports THC companion `ICRS`/`ISUB` method readiness counts (`thc_quicki2c_ready`) during ACPI probe so missing controller-companion surfaces are visible before driver bring-up. -- `hwd` now also emits compact `RB_THC_QUICKI2C_SCHEMA` / `RB_THC_QUICKI2C` marker lines (`status=absent|available|not-ready`) for THC companion readiness scraping in CI/log pipelines. -- `RB_THC_QUICKI2C` now also includes `generation=` for consistent correlation semantics with other marker streams. -- `hwd` now assigns marker generation per ACPI probe pass and threads it through UCSI/THC fallback markers as well, eliminating `generation=0` ambiguity on local fallback paths. -- schema markers now also carry generation (`RB_UCSI_SCHEMA`, `RB_THC_QUICKI2C_SCHEMA`, `RB_UCSID_SCHEMA`, `RB_I2C_HIDD_SCHEMA`, `RB_THC_HIDD_SCHEMA`) so schema and data lines share the same correlation key shape. -- `i2c-hidd` now consults THC companion `ICRS` when bound through `intel-thc-quicki2c`, and performs a bounded slave-address override if HID `_CRS` I2C address and companion-method address disagree. -- `i2c-hidd` now emits compact `RB_I2C_HIDD_SCHEMA` and `RB_I2C_HIDD_BLOCKER` markers so unresolved THC resource-source adapter matches are machine-readable in boot logs (`reason=thc_transport_adapter_unavailable`). -- `i2c-hidd` marker emitters now canonicalize status fields (`RB_I2C_HIDD_SNAPSHOT`, `RB_I2C_HIDD_BLOCKER`) and warn/coerce unknown values to `error` for parser-safe output under schema drift. -- `RB_I2C_HIDD_BLOCKER` now also includes `generation=`, aligned to the current scan cycle so blocker and snapshot events are directly correlatable. -- `RB_I2C_HIDD_BLOCKER` status semantics now also include `not-ready` (`acpi_symbols_not_ready`) and `error` (`acpi_symbol_scan_error`) on the ACPI symbol scan path, aligning HID-consumer readiness reporting with THC producer markers. -- `i2c-hidd` scan-state handling now preserves that distinction in snapshots: ACPI `WouldBlock` yields `RB_I2C_HIDD_SNAPSHOT status=not-ready reason=acpi_symbols_not_ready` (not `not-available`), avoiding false “no devices” classification during early init ordering. -- `i2c-hidd` now emits `RB_I2C_HIDD_SNAPSHOT` per scan cycle (`status`, `reason`, `devices`, `adapters`, `started`, `probe_ok`, `probe_failed`) so boot logs expose HID bring-up progress and failure density, not only blocker edges. -- `RB_I2C_HIDD_SNAPSHOT` now also carries `generation=`, allowing cycle-level correlation with other readiness marker streams. -- the same `RB_I2C_HIDD_SNAPSHOT` stream now includes `status=error reason=scan_failed` on scan-cycle exceptions, preserving explicit machine-readable failure state even when scan aborts early. -- The in-tree bridge now derives adapter speed profile from ACPI-bound devices and includes bounded one-shot controller recovery/reinit on non-addressing transfer failures. -- `ucsid` now exposes `/scheme/ucsi` summary/device records with policy-driven `input_critical` classification, per-node probe policy, bounded AMDI0042 I2C probe telemetry, and PNP0CA0 ACPI `_DSM` capability-mask diagnostics (Linux carrier aligned with `ucsi_acpi.c` function-mask semantics). -- `ucsid` now also supports a policy/env-gated bounded PNP0CA0 `_DSM` function-2 read-call probe (`REDBEAR_UCSI_DSM_READ_PROBE` / `probe_dsm_read`) to surface ACPI transport readiness without enabling full UCSI command transport. -- when that bounded read probe returns a buffer payload, `ucsid` now surfaces a minimal UCSI header snapshot (`version_bcd` at offset 0 and `cci` at offset 4) for early transport-readiness diagnostics. -- `hwd` now opportunistically consumes `/scheme/ucsi/summary` (when available) and logs UCSI transport-readiness snapshot counts/devices on the ACPI probe path. -- `hwd` now classifies UCSI summary availability as `available` / `not-ready` / `not-available` / `error`, making initfs service-order gaps distinguishable from summary parse/probe failures. -- `hwd` now emits a compact `RB_UCSI_SNAPSHOT ...` key-value marker line for CI/log scrapers in addition to the human-readable UCSI status logs. -- `ucsid` now emits compact `RB_UCSID_SUMMARY ...` and per-device `RB_UCSID_DEVICE ...` marker lines so readiness can be scraped directly from daemon logs without scheme reads. -- `hwd` now mirrors per-device compact markers as `RB_UCSI_DEVICE ...` when `/scheme/ucsi/summary` is available, so CI can consume transport-readiness from the `hwd` boot stream as well. -- `ucsid` now classifies `transport_blocker` for `input_critical` devices that are not transport-ready, and exports `transport_blocked_input_critical` in summary markers for boot-priority triage. -- `ucsid`/`hwd` compact markers now include `health=ok|degraded` plus dedicated `RB_UCSID_HEALTH` / `RB_UCSI_HEALTH` lines keyed by `transport_blocked_input_critical`. -- `RB_UCSID_SUMMARY` and mirrored `RB_UCSI_SUMMARY` now carry `generation=` so CI can correlate `hwd` snapshots with a specific `ucsid` scan cycle and detect stale reads. -- generation is now assigned before each `ucsid` scan pass and included on per-device markers (`RB_UCSID_DEVICE` / mirrored `RB_UCSI_DEVICE`) so device-level lines are cycle-correlated as well. -- `RB_UCSI_SNAPSHOT` is now self-contained with `generation`, `health`, and `transport_blocked_input_critical` when summary is available, while preserving explicit `status=*` for not-ready/not-available/error cases. -- `hwd` now emits `RB_UCSI_SNAPSHOT status=absent ...` when no ACPI UCSI candidates are discovered, so CI can distinguish true surface absence from service readiness failures. -- `ucsid` compact markers now emit explicit status on every scan cycle (`status=available|absent|not-ready|error`) via `RB_UCSID_SUMMARY` / `RB_UCSID_HEALTH`. -- `ucsid` now also emits `RB_UCSID_SUMMARY` / `RB_UCSID_HEALTH` with `status=error` and `health=unknown` on scan-cycle failures, so CI can distinguish explicit UCSI scan faults from stale/missing marker streams. -- on scan failure, `ucsid` now also resets shared `/scheme/ucsi/summary` counters to a clean `status=error` state for that generation (instead of carrying stale counters from the previous successful cycle). -- `ucsid` ACPI-symbol `WouldBlock` no longer collapses into `status=absent`; it now emits explicit `status=not-ready` markers so early-init readiness is not misclassified as true UCSI surface absence. -- `/scheme/ucsi/summary` now carries explicit producer status (`available|absent|not-ready|error`) and `hwd` consumes that field when mirroring `RB_UCSI_*`, preventing false `status=available` interpretations from zeroed summary counters during not-ready/error cycles. -- `hwd` now normalizes UCSI summary status parsing (trims whitespace and accepts case variants) and warns before coercing unknown statuses to `error`, improving resilience to producer/schema drift. -- `hwd` now also cross-checks status against summary counters: contradictory payloads (`available` with zero devices, or `absent` with nonzero devices) are warned and coerced to safe marker output (`absent` / `error`) instead of being mirrored verbatim. -- for `status=not-ready|not-available|error`, `hwd` now warns if nonzero payload counters/devices are present before emitting fallback status markers, making producer-status drift explicit in logs. -- `ucsid` startup default summary state is now explicitly `status=not-ready` (instead of implicit empty/default status), so early pre-scan reads of `/scheme/ucsi/summary` remain unambiguous. -- mirrored `hwd` markers now carry explicit status on `RB_UCSI_SUMMARY` / `RB_UCSI_HEALTH` (`available|absent|not-ready|not-available|error`), keeping status semantics aligned across both producers and fallback paths. -- mirrored `RB_UCSI_DEVICE` now carries richer readiness context (`i2c_backed`, DSM read support/probe flags) to match `ucsid` diagnostics from a single boot-log stream. -- for `status=not-ready|not-available|error`, `hwd` now emits fallback `RB_UCSI_SUMMARY` / `RB_UCSI_HEALTH` lines (not just `RB_UCSI_SNAPSHOT`) so CI can rely on the same marker keys in every status path. -- Native THC DMA/report transport is still missing. +## Marker Emission Summary + +The I2C stack uses structured marker lines for CI/log scraping: + +| Producer | Marker | Purpose | +|----------|--------|---------| +| `hwd` | `RB_THC_QUICKI2K_SCHEMA` / `RB_THC_QUICKI2K` | THC companion readiness | +| `hwd` | `RB_UCSI_SCHEMA` / `RB_UCSI_SNAPSHOT` / `RB_UCSI_SUMMARY` / `RB_UCSI_HEALTH` / `RB_UCSI_DEVICE` | UCSI topology readiness | +| `i2c-hidd` | `RB_I2C_HIDD_SCHEMA` / `RB_I2C_HIDD_BLOCKER` / `RB_I2C_HIDD_SNAPSHOT` | HID bind progress and blockers | +| `intel-thc-hidd` | `RB_THC_HIDD_SCHEMA` / `RB_THC_HIDD` / `RB_THC_HIDD_FATAL` | THC transport bring-up status | +| `ucsid` | `RB_UCSID_SCHEMA` / `RB_UCSID_SUMMARY` / `RB_UCSID_DEVICE` / `RB_UCSID_HEALTH` | UCSI daemon diagnostics | + +All markers carry `generation=` for cycle-level correlation across producers. + +## Service Boot Ordering + +``` +00_base.target + → 40_pcid.service (PCI enumeration) + → 41_acpid.service (ACPI tables + AML evaluation) + → 40_hwd.service (hardware discovery + markers) + → 00_i2cd.service (I2C adapter registry) + → 00_i2c-dw-acpi.service (DesignWare I2C controllers) + → 00_intel-gpiod.service (Intel GPIO controller) + → 00_i2c-gpio-expanderd.service (GPIO expander companion) + → 00_i2c-hidd.service (I2C HID devices — touchpads, keyboards) + → 00_ucsid.service (UCSI USB-C topology) +``` + +All I2C services use non-blocking (`oneshot_async`) startup so the boot path is not blocked +by any single service's probe latency. diff --git a/local/patches/base/P2-acpi-i2c-resources.patch b/local/patches/base/P2-acpi-i2c-resources.patch new file mode 100644 index 00000000..5df90963 --- /dev/null +++ b/local/patches/base/P2-acpi-i2c-resources.patch @@ -0,0 +1,1362 @@ +diff --git a/Cargo.toml b/Cargo.toml +index 9e776232..1578e5e9 100644 +--- a/Cargo.toml ++++ b/Cargo.toml +@@ -20,6 +20,7 @@ members = [ + "drivers/common", + "drivers/executor", + ++ "drivers/acpi-resource", + "drivers/acpid", + "drivers/hwd", + "drivers/pcid", +@@ -42,6 +43,12 @@ members = [ + "drivers/graphics/vesad", + "drivers/graphics/virtio-gpud", + ++ "drivers/gpio/i2c-gpio-expanderd", ++ ++ "drivers/i2c/dw-acpi-i2cd", ++ ++ "drivers/input/i2c-hidd", ++ "drivers/input/intel-thc-hidd", + "drivers/input/ps2d", + "drivers/input/usbhidd", + +@@ -65,6 +72,7 @@ members = [ + + "drivers/usb/xhcid", + "drivers/usb/usbctl", ++ "drivers/usb/ucsid", + "drivers/usb/usbhubd", + ] + +diff --git a/drivers/acpid/Cargo.toml b/drivers/acpid/Cargo.toml +index 2d22a8f9..712b6d6e 100644 +--- a/drivers/acpid/Cargo.toml ++++ b/drivers/acpid/Cargo.toml +@@ -8,6 +8,7 @@ edition = "2018" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + [dependencies] ++acpi-resource = { path = "../acpi-resource" } + acpi = { git = "https://github.com/jackpot51/acpi.git" } + arrayvec = "0.7.6" + log.workspace = true +@@ -21,6 +22,7 @@ rustc-hash = "1.1.0" + thiserror.workspace = true + ron.workspace = true + serde.workspace = true ++toml.workspace = true + + amlserde = { path = "../amlserde" } + common = { path = "../common" } +diff --git a/drivers/acpid/src/main.rs b/drivers/acpid/src/main.rs +index 059254b3..3b0deeab 100644 +--- a/drivers/acpid/src/main.rs ++++ b/drivers/acpid/src/main.rs +@@ -5,107 +5,182 @@ use std::ops::ControlFlow; + use std::os::unix::io::AsRawFd; + use std::sync::Arc; + +-use ::acpi::aml::op_region::{RegionHandler, RegionSpace}; + use event::{EventFlags, RawEventQueue}; + use redox_scheme::{scheme::register_sync_scheme, Socket}; + use scheme_utils::Blocking; ++use thiserror::Error; + + mod acpi; + mod aml_physmem; + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + mod ec; + ++mod resources; + mod scheme; ++mod sleep; + +-fn daemon(daemon: daemon::Daemon) -> ! { +- common::setup_logging( +- "misc", +- "acpi", +- "acpid", +- common::output_level(), +- common::file_level(), +- ); ++#[derive(Debug, Error)] ++enum StartupError { ++ #[error("failed to read `/scheme/kernel.acpi/rxsdt`: {0}")] ++ ReadRootTable(std::io::Error), + +- log::info!("acpid start"); ++ #[error("failed to parse [R/X]SDT from kernel: {0}")] ++ ParseRootTable(self::acpi::InvalidSdtError), + +- let rxsdt_raw_data: Arc<[u8]> = std::fs::read("/scheme/kernel.acpi/rxsdt") +- .expect("acpid: failed to read `/scheme/kernel.acpi/rxsdt`") +- .into(); ++ #[error("kernel returned unsupported root table signature `{signature}`")] ++ UnsupportedRootSignature { signature: String }, + +- if rxsdt_raw_data.is_empty() { +- log::info!("System doesn't use ACPI"); +- daemon.ready(); +- std::process::exit(0); ++ #[error( ++ "root table `{signature}` payload length {payload_len} is not divisible by entry size {entry_size}" ++ )] ++ MisalignedRootEntries { ++ signature: String, ++ payload_len: usize, ++ entry_size: usize, ++ }, ++ ++ #[error("{context}: {message}")] ++ Runtime { ++ context: &'static str, ++ message: String, ++ }, ++} ++ ++impl StartupError { ++ fn runtime(context: &'static str, message: impl Into) -> Self { ++ Self::Runtime { ++ context, ++ message: message.into(), ++ } + } ++} + +- let sdt = self::acpi::Sdt::new(rxsdt_raw_data).expect("acpid: failed to parse [RX]SDT"); ++fn parse_root_sdt(raw_data: Arc<[u8]>) -> Result, StartupError> { ++ if raw_data.is_empty() { ++ return Ok(None); ++ } + +- let mut thirty_two_bit; +- let mut sixty_four_bit; ++ self::acpi::Sdt::new(raw_data) ++ .map(Some) ++ .map_err(StartupError::ParseRootTable) ++} ++ ++fn root_table_physaddrs(sdt: &self::acpi::Sdt) -> Result, StartupError> { ++ let signature = String::from_utf8_lossy(&sdt.signature).into_owned(); ++ let data = sdt.data(); + +- let physaddrs_iter = match &sdt.signature { ++ match &sdt.signature { + b"RSDT" => { +- thirty_two_bit = sdt +- .data() +- .chunks(mem::size_of::()) +- // TODO: With const generics, the compiler has some way of doing this for static sizes. +- .map(|chunk| <[u8; mem::size_of::()]>::try_from(chunk).unwrap()) +- .map(|chunk| u32::from_le_bytes(chunk)) +- .map(u64::from); ++ let entry_size = mem::size_of::(); ++ if data.len() % entry_size != 0 { ++ return Err(StartupError::MisalignedRootEntries { ++ signature, ++ payload_len: data.len(), ++ entry_size, ++ }); ++ } + +- &mut thirty_two_bit as &mut dyn Iterator ++ Ok(data ++ .chunks_exact(entry_size) ++ .map(|chunk| <[u8; mem::size_of::()]>::try_from(chunk).unwrap()) ++ .map(u32::from_le_bytes) ++ .map(u64::from) ++ .collect()) + } + b"XSDT" => { +- sixty_four_bit = sdt +- .data() +- .chunks(mem::size_of::()) +- .map(|chunk| <[u8; mem::size_of::()]>::try_from(chunk).unwrap()) +- .map(|chunk| u64::from_le_bytes(chunk)); ++ let entry_size = mem::size_of::(); ++ if data.len() % entry_size != 0 { ++ return Err(StartupError::MisalignedRootEntries { ++ signature, ++ payload_len: data.len(), ++ entry_size, ++ }); ++ } + +- &mut sixty_four_bit as &mut dyn Iterator ++ Ok(data ++ .chunks_exact(entry_size) ++ .map(|chunk| <[u8; mem::size_of::()]>::try_from(chunk).unwrap()) ++ .map(u64::from_le_bytes) ++ .collect()) + } +- _ => panic!("acpid: expected [RX]SDT from kernel to be either of those"), ++ _ => Err(StartupError::UnsupportedRootSignature { signature }), ++ } ++} ++ ++fn run_acpid(daemon: daemon::Daemon) -> Result<(), StartupError> { ++ let rxsdt_raw_data: Arc<[u8]> = std::fs::read("/scheme/kernel.acpi/rxsdt") ++ .map(Arc::<[u8]>::from) ++ .map_err(StartupError::ReadRootTable)?; ++ ++ let Some(sdt) = parse_root_sdt(rxsdt_raw_data)? else { ++ log::info!("System doesn't use ACPI"); ++ daemon.ready(); ++ std::process::exit(0); + }; + +- let region_handlers: Vec<(RegionSpace, Box)> = vec![ +- #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +- (RegionSpace::EmbeddedControl, Box::new(ec::Ec::new())), +- ]; +- let acpi_context = self::acpi::AcpiContext::init(physaddrs_iter, region_handlers); ++ let physaddrs = root_table_physaddrs(&sdt)?; ++ let acpi_context = self::acpi::AcpiContext::init(physaddrs.into_iter()); + +- // TODO: I/O permission bitmap? + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +- common::acquire_port_io_rights().expect("acpid: failed to set I/O privilege level to Ring 3"); ++ common::acquire_port_io_rights().map_err(|error| { ++ StartupError::runtime( ++ "failed to set I/O privilege level to Ring 3", ++ format!("{error}"), ++ ) ++ })?; + +- let shutdown_pipe = File::open("/scheme/kernel.acpi/kstop") +- .expect("acpid: failed to open `/scheme/kernel.acpi/kstop`"); ++ let shutdown_pipe = File::open("/scheme/kernel.acpi/kstop").map_err(|error| { ++ StartupError::runtime( ++ "failed to open `/scheme/kernel.acpi/kstop`", ++ error.to_string(), ++ ) ++ })?; + +- let mut event_queue = RawEventQueue::new().expect("acpid: failed to create event queue"); +- let socket = Socket::nonblock().expect("acpid: failed to create disk scheme"); ++ let mut event_queue = RawEventQueue::new().map_err(|error| { ++ StartupError::runtime("failed to create event queue", error.to_string()) ++ })?; ++ let socket = Socket::nonblock().map_err(|error| { ++ StartupError::runtime("failed to create acpi scheme socket", error.to_string()) ++ })?; + + let mut scheme = self::scheme::AcpiScheme::new(&acpi_context, &socket); + let mut handler = Blocking::new(&socket, 16); + + event_queue + .subscribe(shutdown_pipe.as_raw_fd() as usize, 0, EventFlags::READ) +- .expect("acpid: failed to register shutdown pipe for event queue"); ++ .map_err(|error| { ++ StartupError::runtime( ++ "failed to register shutdown pipe for event queue", ++ error.to_string(), ++ ) ++ })?; + event_queue + .subscribe(socket.inner().raw(), 1, EventFlags::READ) +- .expect("acpid: failed to register scheme socket for event queue"); ++ .map_err(|error| { ++ StartupError::runtime( ++ "failed to register scheme socket for event queue", ++ error.to_string(), ++ ) ++ })?; + +- register_sync_scheme(&socket, "acpi", &mut scheme) +- .expect("acpid: failed to register acpi scheme to namespace"); ++ register_sync_scheme(&socket, "acpi", &mut scheme).map_err(|error| { ++ StartupError::runtime( ++ "failed to register acpi scheme to namespace", ++ error.to_string(), ++ ) ++ })?; + +- daemon.ready(); ++ libredox::call::setrens(0, 0).map_err(|error| { ++ StartupError::runtime("failed to enter null namespace", error.to_string()) ++ })?; + +- libredox::call::setrens(0, 0).expect("acpid: failed to enter null namespace"); ++ daemon.ready(); + + let mut mounted = true; + while mounted { +- let Some(event) = event_queue +- .next() +- .transpose() +- .expect("acpid: failed to read event file") ++ let Some(event) = event_queue.next().transpose().map_err(|error| { ++ StartupError::runtime("failed to read event file", error.to_string()) ++ })? + else { + break; + }; +@@ -114,8 +189,9 @@ fn daemon(daemon: daemon::Daemon) -> ! { + loop { + match handler + .process_requests_nonblocking(&mut scheme) +- .expect("acpid: failed to process requests") +- { ++ .map_err(|error| { ++ StartupError::runtime("failed to process requests", error.to_string()) ++ })? { + ControlFlow::Continue(()) => {} + ControlFlow::Break(()) => break, + } +@@ -125,19 +201,139 @@ fn daemon(daemon: daemon::Daemon) -> ! { + mounted = false; + } else { + log::debug!("Received request to unknown fd: {}", event.fd); +- continue; + } + } + + drop(shutdown_pipe); + drop(event_queue); + +- acpi_context.set_global_s_state(5); ++ acpi_context.set_global_s_state(5).map_err(|error| { ++ StartupError::runtime( ++ "failed to shut down after kernel request", ++ error.to_string(), ++ ) ++ }) ++} ++ ++fn daemon(daemon: daemon::Daemon) -> ! { ++ common::setup_logging( ++ "misc", ++ "acpi", ++ "acpid", ++ common::output_level(), ++ common::file_level(), ++ ); + +- unreachable!("System should have shut down before this is entered"); ++ log::info!("acpid start"); ++ ++ if let Err(error) = run_acpid(daemon) { ++ log::error!("acpid startup/runtime failure: {error}"); ++ std::process::exit(1); ++ } ++ ++ unreachable!("acpid returned from run_acpid without exiting or shutting down"); + } + + fn main() { + common::init(); + daemon::Daemon::new(daemon); + } ++ ++#[cfg(test)] ++mod tests { ++ use super::{parse_root_sdt, root_table_physaddrs, StartupError}; ++ use crate::acpi::SdtHeader; ++ use std::sync::Arc; ++ ++ fn make_sdt(signature: [u8; 4], payload: &[u8]) -> Arc<[u8]> { ++ let length = (std::mem::size_of::() + payload.len()) as u32; ++ let header = SdtHeader { ++ signature, ++ length, ++ revision: 1, ++ checksum: 0, ++ oem_id: *b"REDBAR", ++ oem_table_id: *b"ACPITEST", ++ oem_revision: 1, ++ creator_id: 1, ++ creator_revision: 1, ++ }; ++ ++ let mut bytes = unsafe { plain::as_bytes(&header) }.to_vec(); ++ bytes.extend_from_slice(payload); ++ ++ let checksum = bytes ++ .iter() ++ .copied() ++ .fold(0u8, |sum, byte| sum.wrapping_add(byte)); ++ bytes[9] = 0u8.wrapping_sub(checksum); ++ ++ bytes.into() ++ } ++ ++ #[test] ++ fn empty_root_table_means_no_acpi() { ++ let parsed = parse_root_sdt(Arc::<[u8]>::from(Vec::::new())).unwrap(); ++ assert!(parsed.is_none()); ++ } ++ ++ #[test] ++ fn rsdt_physaddrs_parse_without_panic() { ++ let payload = [ ++ 0x78, 0x56, 0x34, 0x12, // 0x12345678 ++ 0xF0, 0xDE, 0xBC, 0x9A, // 0x9ABCDEF0 ++ ]; ++ let sdt = parse_root_sdt(make_sdt(*b"RSDT", &payload)) ++ .unwrap() ++ .unwrap(); ++ ++ assert_eq!( ++ root_table_physaddrs(&sdt).unwrap(), ++ vec![0x1234_5678, 0x9ABC_DEF0] ++ ); ++ } ++ ++ #[test] ++ fn xsdt_physaddrs_parse_without_panic() { ++ let payload = [ ++ 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, ++ 0xAA, 0x99, ++ ]; ++ let sdt = parse_root_sdt(make_sdt(*b"XSDT", &payload)) ++ .unwrap() ++ .unwrap(); ++ ++ assert_eq!( ++ root_table_physaddrs(&sdt).unwrap(), ++ vec![0x1122_3344_5566_7788, 0x99AA_BBCC_DDEE_FF00] ++ ); ++ } ++ ++ #[test] ++ fn invalid_root_signature_is_explicit() { ++ let sdt = parse_root_sdt(make_sdt(*b"FADT", &[])).unwrap().unwrap(); ++ ++ let error = root_table_physaddrs(&sdt).unwrap_err(); ++ assert!(matches!( ++ error, ++ StartupError::UnsupportedRootSignature { .. } ++ )); ++ } ++ ++ #[test] ++ fn misaligned_rsdt_entries_are_rejected() { ++ let sdt = parse_root_sdt(make_sdt(*b"RSDT", &[1, 2, 3])) ++ .unwrap() ++ .unwrap(); ++ ++ let error = root_table_physaddrs(&sdt).unwrap_err(); ++ assert!(matches!( ++ error, ++ StartupError::MisalignedRootEntries { ++ entry_size: 4, ++ payload_len: 3, ++ .. ++ } ++ )); ++ } ++} +diff --git a/drivers/acpid/src/scheme.rs b/drivers/acpid/src/scheme.rs +index 5a5040c3..8158cad2 100644 +--- a/drivers/acpid/src/scheme.rs ++++ b/drivers/acpid/src/scheme.rs +@@ -2,7 +2,6 @@ use acpi::aml::namespace::AmlName; + use amlserde::aml_serde_name::to_aml_format; + use amlserde::AmlSerdeValue; + use core::str; +-use libredox::Fd; + use parking_lot::RwLockReadGuard; + use redox_scheme::scheme::SchemeSync; + use redox_scheme::{CallerCtx, OpenResult, SendFdRequest, Socket}; +@@ -16,17 +15,22 @@ use syscall::FobtainFdFlags; + + use syscall::data::Stat; + use syscall::error::{Error, Result}; +-use syscall::error::{EACCES, EBADF, EBADFD, EINVAL, EIO, EISDIR, ENOENT, ENOTDIR}; ++use syscall::error::{ ++ EACCES, EAGAIN, EBADF, EBADFD, EINVAL, EIO, EISDIR, ENOENT, ENOTDIR, EOPNOTSUPP, ++}; + use syscall::flag::{MODE_DIR, MODE_FILE}; + use syscall::flag::{O_ACCMODE, O_DIRECTORY, O_RDONLY, O_STAT, O_SYMLINK}; + use syscall::{EOVERFLOW, EPERM}; + +-use crate::acpi::{AcpiContext, AmlSymbols, SdtSignature}; ++use crate::acpi::{ ++ AcpiBattery, AcpiContext, AcpiPowerAdapter, AcpiPowerSnapshot, AmlSymbols, DmiInfo, ++ SdtSignature, ++}; ++use crate::resources::{decode_resource_template, ResourceDescriptor}; + + pub struct AcpiScheme<'acpi, 'sock> { + ctx: &'acpi AcpiContext, + handles: HandleMap>, +- pci_fd: Option, + socket: &'sock Socket, + } + +@@ -41,10 +45,204 @@ enum HandleKind<'a> { + Table(SdtSignature), + Symbols(RwLockReadGuard<'a, AmlSymbols>), + Symbol { name: String, description: String }, ++ ResourcesDir, ++ Resources(String), ++ Reboot, ++ DmiDir, ++ Dmi(String), ++ PowerDir, ++ PowerAdaptersDir, ++ PowerAdapterDir(String), ++ PowerBatteriesDir, ++ PowerBatteryDir(String), ++ PowerFile(String), + SchemeRoot, + RegisterPci, + } + ++const DMI_DIRECTORY_ENTRIES: &[&str] = &[ ++ "sys_vendor", ++ "board_vendor", ++ "board_name", ++ "board_version", ++ "product_name", ++ "product_version", ++ "bios_version", ++ "match_all", ++]; ++ ++fn dmi_contents(dmi_info: Option<&DmiInfo>, name: &str) -> Option { ++ Some(match name { ++ "sys_vendor" => dmi_info ++ .and_then(|info| info.sys_vendor.clone()) ++ .unwrap_or_default(), ++ "board_vendor" => dmi_info ++ .and_then(|info| info.board_vendor.clone()) ++ .unwrap_or_default(), ++ "board_name" => dmi_info ++ .and_then(|info| info.board_name.clone()) ++ .unwrap_or_default(), ++ "board_version" => dmi_info ++ .and_then(|info| info.board_version.clone()) ++ .unwrap_or_default(), ++ "product_name" => dmi_info ++ .and_then(|info| info.product_name.clone()) ++ .unwrap_or_default(), ++ "product_version" => dmi_info ++ .and_then(|info| info.product_version.clone()) ++ .unwrap_or_default(), ++ "bios_version" => dmi_info ++ .and_then(|info| info.bios_version.clone()) ++ .unwrap_or_default(), ++ "match_all" => dmi_info.map(DmiInfo::to_match_lines).unwrap_or_default(), ++ _ => return None, ++ }) ++} ++ ++fn power_bool_contents(value: bool) -> String { ++ if value { ++ String::from("1\n") ++ } else { ++ String::from("0\n") ++ } ++} ++ ++fn power_u64_contents(value: u64) -> String { ++ format!("{value}\n") ++} ++ ++fn power_f64_contents(value: f64) -> String { ++ format!("{value}\n") ++} ++ ++fn power_string_contents(value: &str) -> String { ++ format!("{value}\n") ++} ++ ++fn power_adapter_file_contents(adapter: &AcpiPowerAdapter, name: &str) -> Option { ++ Some(match name { ++ "path" => power_string_contents(&adapter.path), ++ "online" => power_bool_contents(adapter.online), ++ _ => return None, ++ }) ++} ++ ++fn power_adapter_entry_names() -> &'static [&'static str] { ++ &["path", "online"] ++} ++ ++fn power_battery_file_contents(battery: &AcpiBattery, name: &str) -> Option { ++ Some(match name { ++ "path" => power_string_contents(&battery.path), ++ "state" => power_u64_contents(battery.state), ++ "present_rate" => power_u64_contents(battery.present_rate?), ++ "remaining_capacity" => power_u64_contents(battery.remaining_capacity?), ++ "present_voltage" => power_u64_contents(battery.present_voltage?), ++ "power_unit" => power_string_contents(battery.power_unit.as_deref()?), ++ "design_capacity" => power_u64_contents(battery.design_capacity?), ++ "last_full_capacity" => power_u64_contents(battery.last_full_capacity?), ++ "design_voltage" => power_u64_contents(battery.design_voltage?), ++ "technology" => power_string_contents(battery.technology.as_deref()?), ++ "model" => power_string_contents(battery.model.as_deref()?), ++ "serial" => power_string_contents(battery.serial.as_deref()?), ++ "battery_type" => power_string_contents(battery.battery_type.as_deref()?), ++ "oem_info" => power_string_contents(battery.oem_info.as_deref()?), ++ "percentage" => power_f64_contents(battery.percentage?), ++ _ => return None, ++ }) ++} ++ ++fn power_battery_entry_names(battery: &AcpiBattery) -> Vec<&'static str> { ++ let mut names = vec!["path", "state"]; ++ ++ if battery.present_rate.is_some() { ++ names.push("present_rate"); ++ } ++ if battery.remaining_capacity.is_some() { ++ names.push("remaining_capacity"); ++ } ++ if battery.present_voltage.is_some() { ++ names.push("present_voltage"); ++ } ++ if battery.power_unit.is_some() { ++ names.push("power_unit"); ++ } ++ if battery.design_capacity.is_some() { ++ names.push("design_capacity"); ++ } ++ if battery.last_full_capacity.is_some() { ++ names.push("last_full_capacity"); ++ } ++ if battery.design_voltage.is_some() { ++ names.push("design_voltage"); ++ } ++ if battery.technology.is_some() { ++ names.push("technology"); ++ } ++ if battery.model.is_some() { ++ names.push("model"); ++ } ++ if battery.serial.is_some() { ++ names.push("serial"); ++ } ++ if battery.battery_type.is_some() { ++ names.push("battery_type"); ++ } ++ if battery.oem_info.is_some() { ++ names.push("oem_info"); ++ } ++ if battery.percentage.is_some() { ++ names.push("percentage"); ++ } ++ ++ names ++} ++ ++fn top_level_entries(power_available: bool) -> Vec<(&'static str, DirentKind)> { ++ let mut entries = vec![ ++ ("tables", DirentKind::Directory), ++ ("symbols", DirentKind::Directory), ++ ("resources", DirentKind::Directory), ++ ("dmi", DirentKind::Directory), ++ ("reboot", DirentKind::Regular), ++ ]; ++ if power_available { ++ entries.push(("power", DirentKind::Directory)); ++ } ++ entries ++} ++ ++fn resource_symbol_path(path: &str) -> Option { ++ let normalized = path.trim_matches('/').trim_start_matches('\\'); ++ if normalized.is_empty() { ++ return None; ++ } ++ ++ let normalized = normalized.replace('/', "."); ++ if normalized.is_empty() { ++ None ++ } else { ++ Some(format!("{normalized}._CRS")) ++ } ++} ++ ++fn resource_entry_name(symbol: &str) -> Option { ++ symbol ++ .strip_suffix("._CRS") ++ .map(str::to_string) ++ .filter(|path| !path.is_empty()) ++} ++ ++fn resource_dir_entries<'a>(symbols: impl IntoIterator) -> Vec { ++ let mut entries = symbols ++ .into_iter() ++ .filter_map(resource_entry_name) ++ .collect::>(); ++ entries.sort_unstable(); ++ entries.dedup(); ++ entries ++} ++ + impl HandleKind<'_> { + fn is_dir(&self) -> bool { + match self { +@@ -53,6 +251,17 @@ impl HandleKind<'_> { + Self::Table(_) => false, + Self::Symbols(_) => true, + Self::Symbol { .. } => false, ++ Self::ResourcesDir => true, ++ Self::Resources(_) => false, ++ Self::Reboot => false, ++ Self::DmiDir => true, ++ Self::Dmi(_) => false, ++ Self::PowerDir => true, ++ Self::PowerAdaptersDir => true, ++ Self::PowerAdapterDir(_) => true, ++ Self::PowerBatteriesDir => true, ++ Self::PowerBatteryDir(_) => true, ++ Self::PowerFile(_) => false, + Self::SchemeRoot => false, + Self::RegisterPci => false, + } +@@ -65,8 +274,21 @@ impl HandleKind<'_> { + .ok_or(Error::new(EBADFD))? + .length(), + Self::Symbol { description, .. } => description.len(), ++ Self::Resources(contents) => contents.len(), ++ Self::Reboot => 0, ++ Self::Dmi(contents) => contents.len(), ++ Self::PowerFile(contents) => contents.len(), + // Directories +- Self::TopLevel | Self::Symbols(_) | Self::Tables => 0, ++ Self::TopLevel ++ | Self::Symbols(_) ++ | Self::ResourcesDir ++ | Self::Tables ++ | Self::DmiDir ++ | Self::PowerDir ++ | Self::PowerAdaptersDir ++ | Self::PowerAdapterDir(_) ++ | Self::PowerBatteriesDir ++ | Self::PowerBatteryDir(_) => 0, + Self::SchemeRoot | Self::RegisterPci => return Err(Error::new(EBADF)), + }) + } +@@ -77,10 +299,163 @@ impl<'acpi, 'sock> AcpiScheme<'acpi, 'sock> { + Self { + ctx, + handles: HandleMap::new(), +- pci_fd: None, + socket, + } + } ++ ++ fn power_snapshot(&self) -> Result { ++ self.ctx.power_snapshot().map_err(|error| match error { ++ crate::acpi::AmlEvalError::NotInitialized => Error::new(EAGAIN), ++ crate::acpi::AmlEvalError::Unsupported(message) => { ++ log::warn!("ACPI power surface unavailable: {message}"); ++ Error::new(EOPNOTSUPP) ++ } ++ other => { ++ log::warn!("Failed to build ACPI power snapshot: {:?}", other); ++ Error::new(EIO) ++ } ++ }) ++ } ++ ++ fn power_available(&self) -> bool { ++ matches!(self.ctx.power_snapshot(), Ok(_)) ++ } ++ ++ fn resources_handle(&self, path: &str) -> Result> { ++ if !self.ctx.pci_ready() { ++ let display_path = if path.is_empty() { "resources" } else { path }; ++ log::warn!( ++ "Deferring ACPI resource lookup for {display_path} until PCI registration is ready" ++ ); ++ return Err(Error::new(EAGAIN)); ++ } ++ ++ let normalized = path.trim_matches('/'); ++ if normalized.is_empty() { ++ return Ok(HandleKind::ResourcesDir); ++ } ++ ++ let symbol_path = resource_symbol_path(normalized).ok_or(Error::new(ENOENT))?; ++ if self.ctx.aml_lookup(&symbol_path).is_none() { ++ return Err(Error::new(ENOENT)); ++ } ++ ++ let aml_name = ++ AmlName::from_str(&format!("\\{symbol_path}")).map_err(|_| Error::new(ENOENT))?; ++ let buffer = match self.ctx.aml_eval(aml_name, Vec::new()) { ++ Ok(AmlSerdeValue::Buffer(bytes)) => bytes, ++ Ok(other) => { ++ log::debug!( ++ "Skipping ACPI resources for {normalized} due to unexpected _CRS value: {:?}", ++ other ++ ); ++ return Err(Error::new(ENOENT)); ++ } ++ Err(error) => { ++ log::debug!( ++ "Failed to evaluate ACPI resources for {symbol_path}: {:?}", ++ error ++ ); ++ return Err(Error::new(ENOENT)); ++ } ++ }; ++ ++ let descriptors: Vec = ++ decode_resource_template(&buffer).map_err(|error| { ++ log::warn!("Failed to decode ACPI _CRS for {symbol_path}: {error}"); ++ Error::new(EIO) ++ })?; ++ let serialized = ron::ser::to_string(&descriptors).map_err(|error| { ++ log::warn!("Failed to serialize decoded ACPI resources for {symbol_path}: {error}"); ++ Error::new(EIO) ++ })?; ++ ++ Ok(HandleKind::Resources(serialized)) ++ } ++ ++ fn power_handle(&self, path: &str) -> Result> { ++ let normalized = path.trim_matches('/'); ++ self.power_snapshot()?; ++ ++ if normalized.is_empty() { ++ return Ok(HandleKind::PowerDir); ++ } ++ if normalized == "on_battery" { ++ return Ok(HandleKind::PowerFile(power_bool_contents( ++ self.power_snapshot()?.on_battery(), ++ ))); ++ } ++ if normalized == "adapters" { ++ return Ok(HandleKind::PowerAdaptersDir); ++ } ++ if let Some(rest) = normalized.strip_prefix("adapters/") { ++ return self.power_adapter_handle(rest); ++ } ++ if normalized == "batteries" { ++ return Ok(HandleKind::PowerBatteriesDir); ++ } ++ if let Some(rest) = normalized.strip_prefix("batteries/") { ++ return self.power_battery_handle(rest); ++ } ++ ++ Err(Error::new(ENOENT)) ++ } ++ ++ fn power_adapter_handle(&self, path: &str) -> Result> { ++ let normalized = path.trim_matches('/'); ++ if normalized.is_empty() { ++ return Ok(HandleKind::PowerAdaptersDir); ++ } ++ ++ let mut parts = normalized.split('/'); ++ let adapter_id = parts.next().ok_or(Error::new(ENOENT))?; ++ let field = parts.next(); ++ if parts.next().is_some() { ++ return Err(Error::new(ENOENT)); ++ } ++ ++ let snapshot = self.power_snapshot()?; ++ let adapter = snapshot ++ .adapters ++ .iter() ++ .find(|adapter| adapter.id == adapter_id) ++ .ok_or(Error::new(ENOENT))?; ++ ++ match field { ++ None | Some("") => Ok(HandleKind::PowerAdapterDir(adapter.id.clone())), ++ Some(name) => Ok(HandleKind::PowerFile( ++ power_adapter_file_contents(adapter, name).ok_or(Error::new(ENOENT))?, ++ )), ++ } ++ } ++ ++ fn power_battery_handle(&self, path: &str) -> Result> { ++ let normalized = path.trim_matches('/'); ++ if normalized.is_empty() { ++ return Ok(HandleKind::PowerBatteriesDir); ++ } ++ ++ let mut parts = normalized.split('/'); ++ let battery_id = parts.next().ok_or(Error::new(ENOENT))?; ++ let field = parts.next(); ++ if parts.next().is_some() { ++ return Err(Error::new(ENOENT)); ++ } ++ ++ let snapshot = self.power_snapshot()?; ++ let battery = snapshot ++ .batteries ++ .iter() ++ .find(|battery| battery.id == battery_id) ++ .ok_or(Error::new(ENOENT))?; ++ ++ match field { ++ None | Some("") => Ok(HandleKind::PowerBatteryDir(battery.id.clone())), ++ Some(name) => Ok(HandleKind::PowerFile( ++ power_battery_file_contents(battery, name).ok_or(Error::new(ENOENT))?, ++ )), ++ } ++ } + } + + fn parse_hex_digit(hex: u8) -> Option { +@@ -182,49 +557,97 @@ impl SchemeSync for AcpiScheme<'_, '_> { + + let kind = match handle.kind { + HandleKind::SchemeRoot => { +- // TODO: arrayvec +- let components = { +- let mut v = arrayvec::ArrayVec::<&str, 3>::new(); +- let it = path.split('/'); +- for component in it.take(3) { +- v.push(component); +- } +- +- v +- }; +- +- match &*components { +- [""] => HandleKind::TopLevel, +- ["register_pci"] => HandleKind::RegisterPci, +- ["tables"] => HandleKind::Tables, ++ if path == "resources" || path == "resources/" { ++ self.resources_handle("")? ++ } else if let Some(rest) = path.strip_prefix("resources/") { ++ self.resources_handle(rest)? ++ } else { ++ // TODO: arrayvec ++ let components = { ++ let mut v = arrayvec::ArrayVec::<&str, 4>::new(); ++ let it = path.split('/'); ++ for component in it.take(4) { ++ v.push(component); ++ } + +- ["tables", table] => { +- let signature = parse_table(table.as_bytes()).ok_or(Error::new(ENOENT))?; +- HandleKind::Table(signature) +- } ++ v ++ }; ++ ++ match &*components { ++ [""] => HandleKind::TopLevel, ++ ["reboot"] => HandleKind::Reboot, ++ ["dmi"] => { ++ if flag_dir || flag_stat || path.ends_with('/') { ++ HandleKind::DmiDir ++ } else { ++ HandleKind::Dmi( ++ dmi_contents(self.ctx.dmi_info(), "match_all") ++ .expect("match_all should always resolve"), ++ ) ++ } ++ } ++ ["dmi", ""] => HandleKind::DmiDir, ++ ["dmi", field] => HandleKind::Dmi( ++ dmi_contents(self.ctx.dmi_info(), field).ok_or(Error::new(ENOENT))?, ++ ), ++ ["power"] => self.power_handle("")?, ++ ["power", tail] => self.power_handle(tail)?, ++ ["power", a, b] => self.power_handle(&format!("{a}/{b}"))?, ++ ["power", a, b, c] => self.power_handle(&format!("{a}/{b}/{c}"))?, ++ ["register_pci"] => HandleKind::RegisterPci, ++ ["tables"] => HandleKind::Tables, ++ ++ ["tables", table] => { ++ let signature = ++ parse_table(table.as_bytes()).ok_or(Error::new(ENOENT))?; ++ HandleKind::Table(signature) ++ } + +- ["symbols"] => { +- if let Ok(aml_symbols) = self.ctx.aml_symbols(self.pci_fd.as_ref()) { +- HandleKind::Symbols(aml_symbols) +- } else { +- return Err(Error::new(EIO)); ++ ["symbols"] => { ++ if !self.ctx.pci_ready() { ++ log::warn!( ++ "Deferring AML symbol scan until PCI registration is ready" ++ ); ++ return Err(Error::new(EAGAIN)); ++ } ++ if let Ok(aml_symbols) = self.ctx.aml_symbols() { ++ HandleKind::Symbols(aml_symbols) ++ } else { ++ return Err(Error::new(EIO)); ++ } + } +- } + +- ["symbols", symbol] => { +- if let Some(description) = self.ctx.aml_lookup(symbol) { +- HandleKind::Symbol { +- name: (*symbol).to_owned(), +- description, ++ ["symbols", symbol] => { ++ if !self.ctx.pci_ready() { ++ log::warn!( ++ "Deferring AML symbol lookup for {symbol} until PCI registration is ready" ++ ); ++ return Err(Error::new(EAGAIN)); ++ } ++ if let Some(description) = self.ctx.aml_lookup(symbol) { ++ HandleKind::Symbol { ++ name: (*symbol).to_owned(), ++ description, ++ } ++ } else { ++ return Err(Error::new(ENOENT)); + } +- } else { +- return Err(Error::new(ENOENT)); + } +- } + +- _ => return Err(Error::new(ENOENT)), ++ _ => return Err(Error::new(ENOENT)), ++ } ++ } ++ } ++ HandleKind::DmiDir => { ++ if path.is_empty() { ++ HandleKind::DmiDir ++ } else { ++ HandleKind::Dmi( ++ dmi_contents(self.ctx.dmi_info(), path).ok_or(Error::new(ENOENT))?, ++ ) + } + } ++ HandleKind::ResourcesDir => self.resources_handle(path)?, + HandleKind::Symbols(ref aml_symbols) => { + if let Some(description) = aml_symbols.lookup(path) { + HandleKind::Symbol { +@@ -235,6 +658,23 @@ impl SchemeSync for AcpiScheme<'_, '_> { + return Err(Error::new(ENOENT)); + } + } ++ HandleKind::PowerDir => self.power_handle(path)?, ++ HandleKind::PowerAdaptersDir => self.power_adapter_handle(path)?, ++ HandleKind::PowerAdapterDir(ref adapter_id) => { ++ if path.is_empty() { ++ HandleKind::PowerAdapterDir(adapter_id.clone()) ++ } else { ++ self.power_adapter_handle(&format!("{adapter_id}/{path}"))? ++ } ++ } ++ HandleKind::PowerBatteriesDir => self.power_battery_handle(path)?, ++ HandleKind::PowerBatteryDir(ref battery_id) => { ++ if path.is_empty() { ++ HandleKind::PowerBatteryDir(battery_id.clone()) ++ } else { ++ self.power_battery_handle(&format!("{battery_id}/{path}"))? ++ } ++ } + _ => return Err(Error::new(EACCES)), + }; + +@@ -296,7 +736,7 @@ impl SchemeSync for AcpiScheme<'_, '_> { + ) -> Result { + let offset: usize = offset.try_into().map_err(|_| Error::new(EINVAL))?; + +- let handle = self.handles.get_mut(id)?; ++ let handle = self.handles.get(id)?; + + if handle.stat { + return Err(Error::new(EBADF)); +@@ -309,6 +749,9 @@ impl SchemeSync for AcpiScheme<'_, '_> { + .ok_or(Error::new(EBADFD))? + .as_slice(), + HandleKind::Symbol { description, .. } => description.as_bytes(), ++ HandleKind::Resources(contents) => contents.as_bytes(), ++ HandleKind::Dmi(contents) => contents.as_bytes(), ++ HandleKind::PowerFile(contents) => contents.as_bytes(), + _ => return Err(Error::new(EINVAL)), + }; + +@@ -328,13 +771,95 @@ impl SchemeSync for AcpiScheme<'_, '_> { + mut buf: DirentBuf<&'buf mut [u8]>, + opaque_offset: u64, + ) -> Result> { +- let handle = self.handles.get_mut(id)?; ++ let handle = self.handles.get(id)?; + + match &handle.kind { + HandleKind::TopLevel => { +- const TOPLEVEL_ENTRIES: &[&str] = &["tables", "symbols"]; ++ for (idx, (name, kind)) in top_level_entries(self.power_available()) ++ .iter() ++ .enumerate() ++ .skip(opaque_offset as usize) ++ { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name, ++ kind: *kind, ++ })?; ++ } ++ } ++ HandleKind::DmiDir => { ++ for (idx, name) in DMI_DIRECTORY_ENTRIES ++ .iter() ++ .enumerate() ++ .skip(opaque_offset as usize) ++ { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name, ++ kind: DirentKind::Regular, ++ })?; ++ } ++ } ++ HandleKind::ResourcesDir => { ++ let aml_symbols = self.ctx.aml_symbols().map_err(|_| Error::new(EIO))?; ++ let entries = ++ resource_dir_entries(aml_symbols.symbols_cache().keys().map(String::as_str)); ++ for (idx, name) in entries.iter().enumerate().skip(opaque_offset as usize) { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name, ++ kind: DirentKind::Regular, ++ })?; ++ } ++ } ++ HandleKind::PowerDir => { ++ const POWER_ROOT_ENTRIES: &[(&str, DirentKind)] = &[ ++ ("on_battery", DirentKind::Regular), ++ ("adapters", DirentKind::Directory), ++ ("batteries", DirentKind::Directory), ++ ]; ++ ++ for (idx, (name, kind)) in POWER_ROOT_ENTRIES ++ .iter() ++ .enumerate() ++ .skip(opaque_offset as usize) ++ { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name, ++ kind: *kind, ++ })?; ++ } ++ } ++ HandleKind::PowerAdaptersDir => { ++ let snapshot = self.power_snapshot()?; ++ for (idx, adapter) in snapshot ++ .adapters ++ .iter() ++ .enumerate() ++ .skip(opaque_offset as usize) ++ { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name: adapter.id.as_str(), ++ kind: DirentKind::Directory, ++ })?; ++ } ++ } ++ HandleKind::PowerAdapterDir(adapter_id) => { ++ let snapshot = self.power_snapshot()?; ++ let _adapter = snapshot ++ .adapters ++ .iter() ++ .find(|adapter| adapter.id == *adapter_id) ++ .ok_or(Error::new(EIO))?; + +- for (idx, name) in TOPLEVEL_ENTRIES ++ for (idx, name) in power_adapter_entry_names() + .iter() + .enumerate() + .skip(opaque_offset as usize) +@@ -343,10 +868,44 @@ impl SchemeSync for AcpiScheme<'_, '_> { + inode: 0, + next_opaque_id: idx as u64 + 1, + name, ++ kind: DirentKind::Regular, ++ })?; ++ } ++ } ++ HandleKind::PowerBatteriesDir => { ++ let snapshot = self.power_snapshot()?; ++ for (idx, battery) in snapshot ++ .batteries ++ .iter() ++ .enumerate() ++ .skip(opaque_offset as usize) ++ { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name: battery.id.as_str(), + kind: DirentKind::Directory, + })?; + } + } ++ HandleKind::PowerBatteryDir(battery_id) => { ++ let snapshot = self.power_snapshot()?; ++ let battery = snapshot ++ .batteries ++ .iter() ++ .find(|battery| battery.id == *battery_id) ++ .ok_or(Error::new(EIO))?; ++ let entry_names = power_battery_entry_names(battery); ++ ++ for (idx, name) in entry_names.iter().enumerate().skip(opaque_offset as usize) { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name, ++ kind: DirentKind::Regular, ++ })?; ++ } ++ } + HandleKind::Symbols(aml_symbols) => { + for (idx, (symbol_name, _value)) in aml_symbols + .symbols_cache() +@@ -444,6 +1003,38 @@ impl SchemeSync for AcpiScheme<'_, '_> { + Ok(result_len) + } + ++ fn write( ++ &mut self, ++ id: usize, ++ buf: &[u8], ++ _offset: u64, ++ _flags: u32, ++ _ctx: &CallerCtx, ++ ) -> Result { ++ let handle = self.handles.get_mut(id)?; ++ ++ if handle.stat { ++ return Err(Error::new(EBADF)); ++ } ++ if !handle.allowed_to_eval { ++ return Err(Error::new(EPERM)); ++ } ++ ++ match handle.kind { ++ HandleKind::Reboot => { ++ if buf.is_empty() { ++ return Err(Error::new(EINVAL)); ++ } ++ self.ctx.acpi_reboot().map_err(|error| { ++ log::error!("ACPI reboot failed: {error}"); ++ Error::new(EIO) ++ })?; ++ Ok(buf.len()) ++ } ++ _ => Err(Error::new(EBADF)), ++ } ++ } ++ + fn on_sendfd(&mut self, sendfd_request: &SendFdRequest) -> Result { + let id = sendfd_request.id(); + let num_fds = sendfd_request.num_fds(); +@@ -470,10 +1061,8 @@ impl SchemeSync for AcpiScheme<'_, '_> { + } + let new_fd = libredox::Fd::new(new_fd); + +- if self.pci_fd.is_some() { ++ if self.ctx.register_pci_fd(new_fd).is_err() { + return Err(Error::new(EINVAL)); +- } else { +- self.pci_fd = Some(new_fd); + } + + Ok(num_fds) +@@ -483,3 +1072,90 @@ impl SchemeSync for AcpiScheme<'_, '_> { + self.handles.remove(id); + } + } ++ ++#[cfg(test)] ++mod tests { ++ use super::{dmi_contents, resource_dir_entries, resource_symbol_path, top_level_entries}; ++ use crate::acpi::DmiInfo; ++ use syscall::dirent::DirentKind; ++ ++ #[test] ++ fn dmi_contents_exposes_individual_fields_and_match_all() { ++ let dmi_info = DmiInfo { ++ sys_vendor: Some("Framework".to_string()), ++ board_name: Some("FRANMECP01".to_string()), ++ product_name: Some("Laptop 16".to_string()), ++ ..DmiInfo::default() ++ }; ++ ++ assert_eq!( ++ dmi_contents(Some(&dmi_info), "sys_vendor").as_deref(), ++ Some("Framework") ++ ); ++ assert_eq!( ++ dmi_contents(Some(&dmi_info), "board_name").as_deref(), ++ Some("FRANMECP01") ++ ); ++ assert_eq!( ++ dmi_contents(Some(&dmi_info), "product_name").as_deref(), ++ Some("Laptop 16") ++ ); ++ assert_eq!( ++ dmi_contents(Some(&dmi_info), "match_all").as_deref(), ++ Some("sys_vendor=Framework\nboard_name=FRANMECP01\nproduct_name=Laptop 16") ++ ); ++ assert_eq!(dmi_contents(None, "bios_version").as_deref(), Some("")); ++ assert_eq!(dmi_contents(Some(&dmi_info), "unknown"), None); ++ } ++ ++ #[test] ++ fn top_level_entries_always_include_reboot() { ++ let entries = top_level_entries(false); ++ assert!(entries ++ .iter() ++ .any(|(name, kind)| { *name == "reboot" && matches!(kind, DirentKind::Regular) })); ++ } ++ ++ #[test] ++ fn top_level_entries_only_include_power_when_available() { ++ let hidden = top_level_entries(false); ++ let visible = top_level_entries(true); ++ ++ assert!(!hidden.iter().any(|(name, _)| *name == "power")); ++ assert!(visible ++ .iter() ++ .any(|(name, kind)| { *name == "power" && matches!(kind, DirentKind::Directory) })); ++ } ++ ++ #[test] ++ fn top_level_entries_always_include_resources() { ++ let entries = top_level_entries(false); ++ assert!(entries ++ .iter() ++ .any(|(name, kind)| { *name == "resources" && matches!(kind, DirentKind::Directory) })); ++ } ++ ++ #[test] ++ fn resource_symbol_path_accepts_dotted_and_slash_paths() { ++ assert_eq!( ++ resource_symbol_path("_SB.PCI0.I2C0.TPD0").as_deref(), ++ Some("_SB.PCI0.I2C0.TPD0._CRS") ++ ); ++ assert_eq!( ++ resource_symbol_path("\\_SB/PCI0/I2C0/TPD0").as_deref(), ++ Some("_SB.PCI0.I2C0.TPD0._CRS") ++ ); ++ } ++ ++ #[test] ++ fn resource_dir_entries_list_devices_with_crs_suffix() { ++ assert_eq!( ++ resource_dir_entries([ ++ "_SB.PCI0.I2C0.TPD0._CRS", ++ "_SB.PCI0.I2C1.TPD1._HID", ++ "_SB.PCI0.I2C0.TPD0._CRS", ++ ]), ++ vec!["_SB.PCI0.I2C0.TPD0".to_string()] ++ ); ++ } ++} diff --git a/recipes/core/base/P2-acpi-i2c-resources.patch b/recipes/core/base/P2-acpi-i2c-resources.patch new file mode 120000 index 00000000..d5370a7a --- /dev/null +++ b/recipes/core/base/P2-acpi-i2c-resources.patch @@ -0,0 +1 @@ +../../../local/patches/base/P2-acpi-i2c-resources.patch \ No newline at end of file diff --git a/recipes/core/base/recipe.toml b/recipes/core/base/recipe.toml index 6f00d007..7d633dcc 100644 --- a/recipes/core/base/recipe.toml +++ b/recipes/core/base/recipe.toml @@ -1,6 +1,6 @@ [source] git = "https://gitlab.redox-os.org/redox-os/base.git" -patches = ["redox.patch", "P2-boot-runtime-fixes.patch"] +patches = ["redox.patch", "P2-boot-runtime-fixes.patch", "P2-acpi-i2c-resources.patch"] [build] template = "custom"