Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions i18n/en/chwd.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ pass-profile-no-match-install = passed profile does not match with installed pro
available = AVAILABLE
installed = INSTALLED
device = Device
no-profile-device = no profiles for PCI devices found!
no-profile-device = no profiles for devices found!

# console writer
invalid-profile = profile '{$invalid_profile}' is invalid!
all-pci-profiles = All PCI profiles:
installed-pci-profiles = Installed PCI profiles:
all-usb-profiles = All USB profiles:
installed-profiles = Installed profiles:
pci-profiles-not-found = No PCI profiles found!
no-installed-pci-profiles = No installed PCI profiles!
no-installed-profile-device = no installed profile for PCI devices found!
usb-profiles-not-found = No USB profiles found!
no-installed-profiles = No installed profiles!
no-installed-profile-device = no installed profile for devices found!
Comment on lines 24 to +31
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New/renamed Fluent message IDs were introduced here (e.g., installed-profiles, no-installed-profiles, all-usb-profiles, usb-profiles-not-found). Other locale files under i18n/* still only define the old PCI-specific keys, so non-English locales will fall back to English for these messages. Consider updating other translations (or adding backward-compatible aliases) to keep localization coverage complete.

Copilot uses AI. Check for mistakes.
33 changes: 33 additions & 0 deletions profiles/usb/fprint/profiles.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[fprint]
desc = "Fingerprint reader support (fprintd)"
# Vendor IDs of known fingerprint sensor manufacturers supported by libfprint.
# Source: https://fprint.freedesktop.org/supported-devices.html
# 045e Microsoft (fingerprint sensors in Surface devices)
# 0483 STMicroelectronics / UPEK
# 04f3 Elan Microelectronics
# 05ba Digital Persona
# 06cb Synaptics
# 08ff AuthenTec
# 0bda Realtek
# 10a5 FPC (Fingerprint Cards)
# 138a Validity Sensors (Synaptics)
# 147e UPEK
# 1c7a LighTuning / Egis Technology
# 2541 Realtek
# 27c6 Goodix Technology
# 2808 FocalTech
# 298d Next Biometrics
vendor_ids = "045e 0483 04f3 05ba 06cb 08ff 0bda 10a5 138a 147e 1c7a 2541 27c6 2808 298d"
device_ids = "*"
class_ids = "*"
priority = 5
packages = "fprintd"
post_install = """
systemctl enable fprintd.service
mkdir -p /etc/pam.d/sudo.d
echo 'auth sufficient pam_fprintd.so' > /etc/pam.d/sudo.d/50-fprintd.conf
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This profile matches only on vendor_ids with device_ids = "*"/class_ids = "*", so it will match any USB device from these vendors (e.g., many Realtek/Microsoft devices) even when no fingerprint reader is present. Because the profile installs fprintd and modifies PAM, false positives here have significant side effects. Please narrow the match criteria (e.g., vendor+product ID pairs from the supported-devices list, or a stricter device_name_pattern/class constraint) instead of wildcarding device_ids.

Copilot uses AI. Check for mistakes.
"""
post_remove = """
systemctl disable fprintd.service
rm -f /etc/pam.d/sudo.d/50-fprintd.conf
"""
42 changes: 34 additions & 8 deletions src/console_writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,31 +31,39 @@ pub fn handle_arguments_listing(data: &Data, args: &crate::args::Args) {

// List all profiles
if args.list_all {
let all_profiles = &data.all_profiles;
if all_profiles.is_empty() {
let all_pci_profiles = &data.all_pci_profiles;
let all_usb_profiles = &data.all_usb_profiles;
if all_pci_profiles.is_empty() {
print_warn_msg!("pci-profiles-not-found");
} else {
list_profiles(all_profiles, &fl!("all-pci-profiles"));
list_profiles(all_pci_profiles, &fl!("all-pci-profiles"));
}
if all_usb_profiles.is_empty() {
print_warn_msg!("usb-profiles-not-found");
} else {
list_profiles(all_usb_profiles, &fl!("all-usb-profiles"));
}
}

// List installed profiles
if args.list_installed {
let installed_profiles = &data.installed_profiles;
let installed_profiles = data.installed_profiles();
if args.detail {
print_installed_profiles(installed_profiles);
print_installed_profiles(&installed_profiles);
} else if !installed_profiles.is_empty() {
list_profiles(installed_profiles, &fl!("installed-pci-profiles"));
list_profiles(&installed_profiles, &fl!("installed-profiles"));
} else {
print_warn_msg!("no-installed-pci-profiles");
print_warn_msg!("no-installed-profiles");
}
}

// List available profiles
if args.list_available {
let pci_devices = &data.pci_devices;
let usb_devices = &data.usb_devices;
if args.detail {
crate::device_misc::print_available_profiles_in_detail(pci_devices);
crate::device_misc::print_available_profiles_in_detail("PCI", pci_devices);
crate::device_misc::print_available_profiles_in_detail("USB", usb_devices);
} else {
for pci_device in pci_devices {
let available_profiles = &pci_device.get_available_profiles();
Expand All @@ -76,6 +84,24 @@ pub fn handle_arguments_listing(data: &Data, args: &crate::args::Args) {
),
);
}
for usb_device in usb_devices {
let available_profiles = &usb_device.get_available_profiles();
if available_profiles.is_empty() {
continue;
}

list_profiles(
available_profiles,
&format!(
"{} ({}:{}) {} {}:",
usb_device.sysfs_busid,
usb_device.vendor_id,
usb_device.device_id,
usb_device.vendor_name,
usb_device.device_name
),
);
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
pub const CHWD_CONFIG_FILE: &str = "profiles.toml";
pub const CHWD_PCI_CONFIG_DIR: &str = "/var/lib/chwd/db/pci/";
pub const CHWD_PCI_DATABASE_DIR: &str = "/var/lib/chwd/local/pci/";
pub const CHWD_USB_CONFIG_DIR: &str = "/var/lib/chwd/db/usb/";
pub const CHWD_USB_DATABASE_DIR: &str = "/var/lib/chwd/local/usb/";
pub const CHWD_SCRIPT_PATH: &str = "/var/lib/chwd/scripts/chwd";

pub const CHWD_PM_CACHE_DIR: &str = "/var/cache/pacman/pkg";
Expand Down
133 changes: 118 additions & 15 deletions src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ pub struct Data {
pub sync_package_manager_database: bool,
pub is_ai_sdk_target: bool,
pub pci_devices: ListOfDevicesT,
pub installed_profiles: ListOfProfilesT,
pub all_profiles: ListOfProfilesT,
pub usb_devices: ListOfDevicesT,
pub installed_pci_profiles: ListOfProfilesT,
pub installed_usb_profiles: ListOfProfilesT,
pub all_pci_profiles: ListOfProfilesT,
pub all_usb_profiles: ListOfProfilesT,
pub invalid_profiles: Vec<String>,
}

Expand All @@ -41,6 +44,7 @@ impl Data {
pub fn new(is_ai_sdk: bool) -> Self {
let mut res = Self {
pci_devices: fill_devices().expect("Failed to init"),
usb_devices: fill_usb_devices(),
sync_package_manager_database: true,
is_ai_sdk_target: is_ai_sdk,
..Default::default()
Expand All @@ -50,44 +54,86 @@ impl Data {
res
}

/// Returns combined list of all installed profiles (PCI + USB).
#[must_use]
pub fn installed_profiles(&self) -> Vec<Profile> {
self.installed_pci_profiles
.iter()
.chain(self.installed_usb_profiles.iter())
.cloned()
.collect()
}

/// Returns combined list of all available profiles (PCI + USB).
#[must_use]
pub fn all_profiles(&self) -> Vec<Profile> {
self.all_pci_profiles.iter().chain(self.all_usb_profiles.iter()).cloned().collect()
Comment on lines +60 to +70
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

installed_profiles()/all_profiles() now concatenate the PCI and USB vectors without re-sorting, which can break the previous global priority ordering (previously a single list was sorted by priority). Consider sorting the merged Vec by descending priority (and possibly secondary key like name) before returning to keep behavior stable.

Suggested change
self.installed_pci_profiles
.iter()
.chain(self.installed_usb_profiles.iter())
.cloned()
.collect()
}
/// Returns combined list of all available profiles (PCI + USB).
#[must_use]
pub fn all_profiles(&self) -> Vec<Profile> {
self.all_pci_profiles.iter().chain(self.all_usb_profiles.iter()).cloned().collect()
let mut profiles: Vec<Profile> = self
.installed_pci_profiles
.iter()
.chain(self.installed_usb_profiles.iter())
.cloned()
.collect();
// Preserve global ordering by sorting by descending priority,
// and then by name as a stable secondary key.
profiles.sort_by(|a, b| {
b.priority
.cmp(&a.priority)
.then_with(|| a.name.cmp(&b.name))
});
profiles
}
/// Returns combined list of all available profiles (PCI + USB).
#[must_use]
pub fn all_profiles(&self) -> Vec<Profile> {
let mut profiles: Vec<Profile> = self
.all_pci_profiles
.iter()
.chain(self.all_usb_profiles.iter())
.cloned()
.collect();
// Preserve global ordering by sorting by descending priority,
// and then by name as a stable secondary key.
profiles.sort_by(|a, b| {
b.priority
.cmp(&a.priority)
.then_with(|| a.name.cmp(&b.name))
});
profiles

Copilot uses AI. Check for mistakes.
}

pub fn update_installed_profile_data(&mut self) {
// Clear profile Vec's in each device element
for pci_device in &mut self.pci_devices {
pci_device.installed_profiles.clear();
}
for usb_device in &mut self.usb_devices {
usb_device.installed_profiles.clear();
}

self.installed_profiles.clear();
self.installed_pci_profiles.clear();
self.installed_usb_profiles.clear();

// Refill data
self.fill_installed_profiles();

set_matching_profiles(&mut self.pci_devices, &self.installed_profiles, true);
set_matching_profiles(&mut self.pci_devices, &self.installed_pci_profiles, true);
set_matching_profiles(&mut self.usb_devices, &self.installed_usb_profiles, true);
}

fn fill_installed_profiles(&mut self) {
let conf_path = crate::consts::CHWD_PCI_DATABASE_DIR;
let configs = &mut self.installed_profiles;

fill_profiles(configs, &mut self.invalid_profiles, conf_path, self.is_ai_sdk_target);
fill_profiles(
&mut self.installed_pci_profiles,
&mut self.invalid_profiles,
crate::consts::CHWD_PCI_DATABASE_DIR,
self.is_ai_sdk_target,
);
fill_profiles(
&mut self.installed_usb_profiles,
&mut self.invalid_profiles,
crate::consts::CHWD_USB_DATABASE_DIR,
self.is_ai_sdk_target,
);
}

fn fill_all_profiles(&mut self) {
let conf_path = crate::consts::CHWD_PCI_CONFIG_DIR;
let configs = &mut self.all_profiles;

fill_profiles(configs, &mut self.invalid_profiles, conf_path, self.is_ai_sdk_target);
fill_profiles(
&mut self.all_pci_profiles,
&mut self.invalid_profiles,
crate::consts::CHWD_PCI_CONFIG_DIR,
self.is_ai_sdk_target,
);
fill_profiles(
&mut self.all_usb_profiles,
&mut self.invalid_profiles,
crate::consts::CHWD_USB_CONFIG_DIR,
self.is_ai_sdk_target,
);
}

fn update_profiles_data(&mut self) {
for pci_device in &mut self.pci_devices {
pci_device.available_profiles.clear();
}
for usb_device in &mut self.usb_devices {
usb_device.available_profiles.clear();
}

self.all_profiles.clear();
self.all_pci_profiles.clear();
self.all_usb_profiles.clear();

self.fill_all_profiles();

set_matching_profiles(&mut self.pci_devices, &self.all_profiles, false);
set_matching_profiles(&mut self.pci_devices, &self.all_pci_profiles, false);
set_matching_profiles(&mut self.usb_devices, &self.all_usb_profiles, false);

self.update_installed_profile_data();
}
Expand All @@ -99,7 +145,11 @@ fn fill_profiles(
conf_path: &str,
is_ai_sdk: bool,
) {
for entry in fs::read_dir(conf_path).expect("Failed to read directory!") {
let dir_entries = match fs::read_dir(conf_path) {
Ok(entries) => entries,
Err(_) => return,
};
for entry in dir_entries {
let config_file_path = format!(
"{}/{}",
entry.as_ref().unwrap().path().as_os_str().to_str().unwrap(),
Expand Down Expand Up @@ -181,6 +231,59 @@ fn fill_devices() -> Option<ListOfDevicesT> {
Some(devices)
}

fn fill_usb_devices() -> ListOfDevicesT {
let usb_devices_path = Path::new("/sys/bus/usb/devices");
let dir_entries = match fs::read_dir(usb_devices_path) {
Ok(entries) => entries,
Err(_) => return vec![],
};

let read_sysfs = |path: &Path, attr: &str| -> Option<String> {
fs::read_to_string(path.join(attr)).ok().map(|s| s.trim().to_owned())
};

let mut devices = vec![];

for entry in dir_entries.filter_map(Result::ok) {
let path = entry.path();

// Only consider actual USB devices (have idVendor)
let vendor_id = match read_sysfs(&path, "idVendor") {
Some(v) => v,
None => continue,
};
let device_id = match read_sysfs(&path, "idProduct") {
Some(v) => v,
None => continue,
};

// Skip USB root hubs (vendor 1d6b = Linux Foundation)
if vendor_id == "1d6b" {
continue;
}

let vendor_name = read_sysfs(&path, "manufacturer").unwrap_or_default();
let device_name = read_sysfs(&path, "product").unwrap_or_default();
let class_id = read_sysfs(&path, "bDeviceClass").unwrap_or_default();
let sysfs_busid = entry.file_name().to_string_lossy().into_owned();

devices.push(Device {
class_name: String::new(),
device_name,
vendor_name,
class_id,
device_id,
vendor_id,
sysfs_busid,
sysfs_id: String::new(),
available_profiles: vec![],
installed_profiles: vec![],
});
}

devices
}

fn set_matching_profile(profile: &Profile, devices: &mut ListOfDevicesT, set_as_installed: bool) {
let found_indices: Vec<usize> = get_all_devices_of_profile(devices, profile);

Expand Down
4 changes: 2 additions & 2 deletions src/device_misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
use crate::device::Device;
use crate::{console_writer, fl, profile_misc};

pub fn print_available_profiles_in_detail(devices: &[Device]) {
pub fn print_available_profiles_in_detail(bus_type: &str, devices: &[Device]) {
let mut config_found = false;
for device in devices {
let available_profiles = &device.available_profiles;
Expand All @@ -29,7 +29,7 @@ pub fn print_available_profiles_in_detail(devices: &[Device]) {

log::info!(
"{} {}: {} ({}:{}:{})",
"PCI",
bus_type,
fl!("device"),
device.sysfs_id,
device.class_id,
Expand Down
Loading
Loading