quirks: ACPI DMI infrastructure (R11 part 1)
Phase R11 (2026-06-07) — ACPI DMI rules first commit. Lands
the infrastructure pieces (flag type, rule struct,
TOML parser, lookup function) needed for runtime ACPI
quirk matching against system DMI data. The data side
arrives in the follow-up commit (four quirks.d/*.toml
files).
Changes:
1. AcpiQuirkFlags bitflags (mod.rs:225) with 13 bits
sourced from Linux 7.1:
OSI_DISABLE_{LINUX,VISTA,WIN7,WIN8} — drivers/acpi/osi.c
SLEEP_{OLD_ORDERING,NVS_NOSAVE,DEFAULT_S3} — drivers/acpi/sleep.c
LID_INIT_{DISABLED,OPEN} — drivers/acpi/button.c
BATTERY_{BIX_BROKEN_PACKAGE,
NOTIFICATION_DELAY,
AC_IS_BROKEN} — drivers/acpi/battery.c
REV_OVERRIDE — drivers/acpi/x86/blacklist.c
2. DmiAcpiQuirkRule (dmi.rs:476) — DMI match + flag word,
mirrors the existing DmiXhciQuirkRule shape.
3. DMI_ACPI_QUIRK_RULES — empty compiled-in table for now;
runtime TOML is the data surface (R11 part 2). The
constant exists so the three-layer lookup shape is
stable from day one.
4. load_dmi_acpi_quirks() — reads live SMBIOS, applies
compiled-in + TOML rules, returns AcpiQuirkFlags.
Pattern mirrors load_dmi_xhci_quirks (R7-B).
5. apply_dmi_acpi_quirk_rules() — pure function, OR-
accumulates matching rules. Mirrors
apply_dmi_xhci_quirk_rules.
6. ACPI_FLAG_NAMES + parse_dmi_acpi_toml + load_dmi_acpi_quirks
in toml_loader.rs. New TOML table type
[[dmi_acpi_quirk]] with sub-table +
array of strings.
7. Two unit tests in dmi.rs: empty result for no match,
OR-accumulation for partial match (one rule fires
one flag, the other fires another — both must land).
cargo test: 122/122 (was 120, +2 for the new tests).
cargo check: clean.
cargo clippy: no new warnings in this code.
The data side (46-acpi-sleep.toml, 47-acpi-button.toml,
48-acpi-battery.toml) lands in the follow-up commit.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
use super::{toml_loader, PciQuirkFlags, XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID};
|
use super::{toml_loader, AcpiQuirkFlags, PciQuirkFlags, XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID};
|
||||||
use crate::pci::PciDeviceInfo;
|
use crate::pci::PciDeviceInfo;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
@@ -466,6 +466,64 @@ pub fn load_dmi_xhci_quirks(
|
|||||||
Ok(flags)
|
Ok(flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One DMI-conditioned ACPI rule. Loaded from compiled-in tables
|
||||||
|
/// or runtime `quirks.d/*.toml` `[[dmi_acpi_quirk]]` entries.
|
||||||
|
///
|
||||||
|
/// Phase R11 (2026-06-07) — Linux 7.1 source-of-truth:
|
||||||
|
/// `drivers/acpi/osi.c` (OSI disable), `drivers/acpi/sleep.c`
|
||||||
|
/// (old ordering, NVS save), `drivers/acpi/button.c` (lid init),
|
||||||
|
/// `drivers/acpi/battery.c` (BIX broken, AC broken),
|
||||||
|
/// `drivers/acpi/x86/blacklist.c` (ACPI _REV override).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DmiAcpiQuirkRule {
|
||||||
|
pub dmi_match: DmiMatchRule,
|
||||||
|
pub flags: AcpiQuirkFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compiled-in DMI-based ACPI rules. Mined from Linux 7.1 sources
|
||||||
|
/// referenced in the struct doc-comment above. Phase R11 (2026-06-07)
|
||||||
|
/// initial commit carries 0 entries; runtime TOML files are the
|
||||||
|
/// data surface for this phase. The constant exists so the lookup
|
||||||
|
/// shape is stable from day one.
|
||||||
|
pub const DMI_ACPI_QUIRK_RULES: &[DmiAcpiQuirkRule] = &[];
|
||||||
|
|
||||||
|
/// Look up DMI-based ACPI quirks for the host system.
|
||||||
|
///
|
||||||
|
/// Reads live SMBIOS data via [`read_dmi_info`], then OR-accumulates
|
||||||
|
/// flags from the compiled-in [`DMI_ACPI_QUIRK_RULES`] and any
|
||||||
|
/// matching rules loaded from runtime TOML files. Returns an empty
|
||||||
|
/// set if SMBIOS data is not available.
|
||||||
|
pub fn load_dmi_acpi_quirks() -> AcpiQuirkFlags {
|
||||||
|
let dmi_info = match read_dmi_info() {
|
||||||
|
Ok(info) => info,
|
||||||
|
Err(()) => return AcpiQuirkFlags::empty(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut flags = apply_dmi_acpi_quirk_rules(&dmi_info, DMI_ACPI_QUIRK_RULES);
|
||||||
|
|
||||||
|
if let Ok(toml_flags) = toml_loader::load_dmi_acpi_quirks(&dmi_info) {
|
||||||
|
flags |= toml_flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
flags
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk a slice of `DmiAcpiQuirkRule` and OR-accumulate the flags
|
||||||
|
/// of every rule whose DMI match succeeds. Mirrors
|
||||||
|
/// `apply_dmi_xhci_quirk_rules` (R7-B) and `apply_dmi_pci_quirks`.
|
||||||
|
pub fn apply_dmi_acpi_quirk_rules(
|
||||||
|
dmi_info: &DmiInfo,
|
||||||
|
rules: &[DmiAcpiQuirkRule],
|
||||||
|
) -> AcpiQuirkFlags {
|
||||||
|
let mut flags = AcpiQuirkFlags::empty();
|
||||||
|
for rule in rules {
|
||||||
|
if rule.dmi_match.matches(dmi_info) {
|
||||||
|
flags |= rule.flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flags
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -720,4 +778,56 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert!(!rule.is_empty());
|
assert!(!rule.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Phase R11 — `apply_dmi_acpi_quirk_rules` returns empty when
|
||||||
|
/// no rules match.
|
||||||
|
#[test]
|
||||||
|
fn phase_r11_apply_dmi_acpi_quirk_rules_no_match_returns_empty() {
|
||||||
|
let info = DmiInfo {
|
||||||
|
sys_vendor: Some("Framework".to_string()),
|
||||||
|
product_name: Some("Laptop 16".to_string()),
|
||||||
|
..DmiInfo::default()
|
||||||
|
};
|
||||||
|
let rules = [DmiAcpiQuirkRule {
|
||||||
|
dmi_match: DmiMatchRule {
|
||||||
|
sys_vendor: Some(Cow::Borrowed("Apple Inc.")),
|
||||||
|
..DmiMatchRule::default()
|
||||||
|
},
|
||||||
|
flags: AcpiQuirkFlags::OSI_DISABLE_VISTA,
|
||||||
|
}];
|
||||||
|
let flags = apply_dmi_acpi_quirk_rules(&info, &rules);
|
||||||
|
assert!(flags.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase R11 — matching DMI rule fires its flag.
|
||||||
|
#[test]
|
||||||
|
fn phase_r11_apply_dmi_acpi_quirk_rules_match_or_accumulates() {
|
||||||
|
let info = DmiInfo {
|
||||||
|
sys_vendor: Some("Dell Inc.".to_string()),
|
||||||
|
product_name: Some("Latitude E7270".to_string()),
|
||||||
|
..DmiInfo::default()
|
||||||
|
};
|
||||||
|
let rules = [
|
||||||
|
DmiAcpiQuirkRule {
|
||||||
|
dmi_match: DmiMatchRule {
|
||||||
|
sys_vendor: Some(Cow::Borrowed("Dell Inc.")),
|
||||||
|
product_name: Some(Cow::Borrowed("Latitude E7270")),
|
||||||
|
..DmiMatchRule::default()
|
||||||
|
},
|
||||||
|
flags: AcpiQuirkFlags::SLEEP_OLD_ORDERING,
|
||||||
|
},
|
||||||
|
DmiAcpiQuirkRule {
|
||||||
|
dmi_match: DmiMatchRule {
|
||||||
|
sys_vendor: Some(Cow::Borrowed("Dell Inc.")),
|
||||||
|
..DmiMatchRule::default()
|
||||||
|
},
|
||||||
|
flags: AcpiQuirkFlags::LID_INIT_DISABLED,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let flags = apply_dmi_acpi_quirk_rules(&info, &rules);
|
||||||
|
assert_eq!(
|
||||||
|
flags,
|
||||||
|
AcpiQuirkFlags::SLEEP_OLD_ORDERING | AcpiQuirkFlags::LID_INIT_DISABLED
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,6 +212,48 @@ bitflags::bitflags! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bitflags::bitflags! {
|
||||||
|
/// Flags for ACPI subsystem quirks, evaluated against the
|
||||||
|
/// system DMI information published by acpid at
|
||||||
|
/// `/scheme/acpi/dmi`. Mined from Linux 7.1 sources:
|
||||||
|
/// - `drivers/acpi/osi.c` (OSI disable on Vista / Win7 / Win8)
|
||||||
|
/// - `drivers/acpi/sleep.c` (old suspend ordering, NVS save)
|
||||||
|
/// - `drivers/acpi/button.c` (lid init state)
|
||||||
|
/// - `drivers/acpi/battery.c` (BIX broken, AC broken)
|
||||||
|
/// - `drivers/acpi/x86/blacklist.c` (ACPI _REV override)
|
||||||
|
///
|
||||||
|
/// Phase R11 (2026-06-07). All bits are observability-only
|
||||||
|
/// at this revision — no consumer reads the flag word yet.
|
||||||
|
/// Future revisions can wire acpid / acpi-handlers to react
|
||||||
|
/// to specific bits (e.g. `OSI_DISABLE_VISTA` triggers
|
||||||
|
/// `acpi_osi_setup("!Windows 2006")`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct AcpiQuirkFlags: u64 {
|
||||||
|
// OSI override (osi.c)
|
||||||
|
const OSI_DISABLE_LINUX = 1 << 0;
|
||||||
|
const OSI_DISABLE_VISTA = 1 << 1;
|
||||||
|
const OSI_DISABLE_WIN7 = 1 << 2;
|
||||||
|
const OSI_DISABLE_WIN8 = 1 << 3;
|
||||||
|
|
||||||
|
// Sleep (sleep.c)
|
||||||
|
const SLEEP_OLD_ORDERING = 1 << 4;
|
||||||
|
const SLEEP_NVS_NOSAVE = 1 << 5;
|
||||||
|
const SLEEP_DEFAULT_S3 = 1 << 6;
|
||||||
|
|
||||||
|
// Button (button.c)
|
||||||
|
const LID_INIT_DISABLED = 1 << 7;
|
||||||
|
const LID_INIT_OPEN = 1 << 8;
|
||||||
|
|
||||||
|
// Battery (battery.c)
|
||||||
|
const BATTERY_BIX_BROKEN_PACKAGE = 1 << 9;
|
||||||
|
const BATTERY_NOTIFICATION_DELAY = 1 << 10;
|
||||||
|
const BATTERY_AC_IS_BROKEN = 1 << 11;
|
||||||
|
|
||||||
|
// ACPI rev override (x86/blacklist.c)
|
||||||
|
const REV_OVERRIDE = 1 << 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Wildcard value for PCI ID matching.
|
/// Wildcard value for PCI ID matching.
|
||||||
pub const PCI_QUIRK_ANY_ID: u16 = 0xFFFF;
|
pub const PCI_QUIRK_ANY_ID: u16 = 0xFFFF;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use super::{
|
use super::{
|
||||||
dmi::{self, DmiInfo, DmiMatchRule, DmiPciQuirkRule, DmiXhciQuirkRule},
|
dmi::{self, DmiAcpiQuirkRule, DmiInfo, DmiMatchRule, DmiPciQuirkRule, DmiXhciQuirkRule},
|
||||||
HidQuirkEntry, HidQuirkFlags, MaskWidth, PciQuirkEntry, PciQuirkFlags, PciQuirkLookup,
|
AcpiQuirkFlags, HidQuirkEntry, HidQuirkFlags, MaskWidth, PciQuirkEntry, PciQuirkFlags,
|
||||||
PciQuirkPhase, QuirkAction, UsbQuirkEntry, UsbQuirkFlags, XhciControllerQuirk,
|
PciQuirkLookup, PciQuirkPhase, QuirkAction, UsbQuirkEntry, UsbQuirkFlags,
|
||||||
XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID,
|
XhciControllerQuirk, XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID,
|
||||||
};
|
};
|
||||||
use crate::pci::PciDeviceInfo;
|
use crate::pci::PciDeviceInfo;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
@@ -180,6 +180,82 @@ pub(crate) fn load_dmi_xhci_quirks(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Phase R11 — `dmi_acpi_quirk` flag name table. Mined from
|
||||||
|
/// Linux 7.1 `drivers/acpi/osi.c`, `sleep.c`, `button.c`,
|
||||||
|
/// `battery.c`, and `x86/blacklist.c`.
|
||||||
|
pub const ACPI_FLAG_NAMES: &[(&str, AcpiQuirkFlags)] = &[
|
||||||
|
("osi_disable_linux", AcpiQuirkFlags::OSI_DISABLE_LINUX),
|
||||||
|
("osi_disable_vista", AcpiQuirkFlags::OSI_DISABLE_VISTA),
|
||||||
|
("osi_disable_win7", AcpiQuirkFlags::OSI_DISABLE_WIN7),
|
||||||
|
("osi_disable_win8", AcpiQuirkFlags::OSI_DISABLE_WIN8),
|
||||||
|
("sleep_old_ordering", AcpiQuirkFlags::SLEEP_OLD_ORDERING),
|
||||||
|
("sleep_nvs_nosave", AcpiQuirkFlags::SLEEP_NVS_NOSAVE),
|
||||||
|
("sleep_default_s3", AcpiQuirkFlags::SLEEP_DEFAULT_S3),
|
||||||
|
("lid_init_disabled", AcpiQuirkFlags::LID_INIT_DISABLED),
|
||||||
|
("lid_init_open", AcpiQuirkFlags::LID_INIT_OPEN),
|
||||||
|
("battery_bix_broken_package", AcpiQuirkFlags::BATTERY_BIX_BROKEN_PACKAGE),
|
||||||
|
("battery_notification_delay", AcpiQuirkFlags::BATTERY_NOTIFICATION_DELAY),
|
||||||
|
("battery_ac_is_broken", AcpiQuirkFlags::BATTERY_AC_IS_BROKEN),
|
||||||
|
("rev_override", AcpiQuirkFlags::REV_OVERRIDE),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub(crate) fn read_toml_dmi_acpi_entries() -> std::io::Result<Vec<DmiAcpiQuirkRule>> {
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
for path in sorted_toml_files(QUIRKS_DIR)? {
|
||||||
|
let path_str = path.display().to_string();
|
||||||
|
let content = match std::fs::read_to_string(&path) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("quirks: failed to read {path_str}: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let doc = match content.parse::<toml::Value>() {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("quirks: failed to parse {path_str}: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
parse_dmi_acpi_toml(&doc, &mut entries, &path_str);
|
||||||
|
}
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_dmi_acpi_toml(
|
||||||
|
doc: &toml::Value,
|
||||||
|
out: &mut Vec<DmiAcpiQuirkRule>,
|
||||||
|
path: &str,
|
||||||
|
) {
|
||||||
|
let Some(arr) = doc.get("dmi_acpi_quirk").and_then(|v| v.as_array()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for item in arr {
|
||||||
|
let Some(table) = item.as_table() else {
|
||||||
|
log::warn!("quirks: {path}: dmi_acpi_quirk entry is not a table, skipping");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(match_table) = table.get("match").and_then(|v| v.as_table()) else {
|
||||||
|
log::warn!("quirks: {path}: dmi_acpi_quirk entry is missing match table, skipping");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(dmi_match) = parse_dmi_match_rule(match_table, path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let flags = parse_flags(table, path, "ACPI", ACPI_FLAG_NAMES);
|
||||||
|
out.push(DmiAcpiQuirkRule { dmi_match, flags });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up DMI-based ACPI flags contributed by runtime TOML quirk
|
||||||
|
/// files. Returns the OR-accumulated flags across every matching
|
||||||
|
/// `[[dmi_acpi_quirk]]` entry, or an empty set if no files are
|
||||||
|
/// present.
|
||||||
|
pub(crate) fn load_dmi_acpi_quirks(dmi_info: &DmiInfo) -> Result<AcpiQuirkFlags, ()> {
|
||||||
|
let entries = read_toml_dmi_acpi_entries().map_err(|_| ())?;
|
||||||
|
Ok(dmi::apply_dmi_acpi_quirk_rules(dmi_info, &entries))
|
||||||
|
}
|
||||||
|
|
||||||
fn bounded_u16(val: &toml::Value, field: &str, path: &str) -> Option<u16> {
|
fn bounded_u16(val: &toml::Value, field: &str, path: &str) -> Option<u16> {
|
||||||
match val.as_integer() {
|
match val.as_integer() {
|
||||||
Some(v) => u16::try_from(v).ok().or_else(|| {
|
Some(v) => u16::try_from(v).ok().or_else(|| {
|
||||||
|
|||||||
Reference in New Issue
Block a user