|
1 | 1 | use async_trait::async_trait; |
| 2 | +use std::collections::HashSet; |
2 | 3 | use std::fmt; |
3 | 4 | #[allow(unused_imports)] |
4 | 5 | use tracing::{debug, info, instrument, trace}; |
5 | 6 |
|
6 | 7 | use crate::{ |
7 | | - transport::{device::Device, Channel, ChannelSettings}, |
| 8 | + transport::{device::Device, hid::HidDevice, Channel, ChannelSettings, UsbDeviceId}, |
8 | 9 | webauthn::Error, |
9 | 10 | }; |
10 | 11 |
|
@@ -60,6 +61,17 @@ impl NfcDevice { |
60 | 61 | } |
61 | 62 | } |
62 | 63 |
|
| 64 | + /// The USB (bus, address) backing this device, when it can be resolved. |
| 65 | + /// Performs blocking PC/SC I/O (a `Direct`-mode connect). |
| 66 | + pub fn usb_device_id(&self) -> Option<UsbDeviceId> { |
| 67 | + match &self.info { |
| 68 | + #[cfg(feature = "nfc-backend-pcsc")] |
| 69 | + DeviceInfo::Pcsc(info) => info.usb_device_id(), |
| 70 | + #[cfg(feature = "nfc-backend-libnfc")] |
| 71 | + DeviceInfo::LibNfc(_) => None, |
| 72 | + } |
| 73 | + } |
| 74 | + |
63 | 75 | async fn channel_sync(&self, settings: ChannelSettings) -> Result<NfcChannel<Context>, Error> { |
64 | 76 | trace!("nfc channel {:?}", self); |
65 | 77 | let mut channel: NfcChannel<Context> = match &self.info { |
@@ -135,3 +147,65 @@ pub fn is_nfc_available() -> bool { |
135 | 147 |
|
136 | 148 | available |
137 | 149 | } |
| 150 | + |
| 151 | +/// Lists all NFC devices from the compiled backends, unfiltered. A failing |
| 152 | +/// backend is skipped. Cross-backend duplicates are not removed. |
| 153 | +#[instrument] |
| 154 | +pub async fn list_devices() -> Vec<NfcDevice> { |
| 155 | + #[allow(unused_mut)] |
| 156 | + let mut devices = Vec::new(); |
| 157 | + #[cfg(feature = "nfc-backend-libnfc")] |
| 158 | + if let Ok(found) = libnfc::list_devices() { |
| 159 | + devices.extend(found); |
| 160 | + } |
| 161 | + #[cfg(feature = "nfc-backend-pcsc")] |
| 162 | + if let Ok(found) = pcsc::list_devices() { |
| 163 | + devices.extend(found); |
| 164 | + } |
| 165 | + devices |
| 166 | +} |
| 167 | + |
| 168 | +/// Drops NFC devices that are the CCID face of a USB key already seen over HID, |
| 169 | +/// matched by USB (bus, address). Does blocking PC/SC I/O per reader. |
| 170 | +pub trait NfcDeviceSliceExt { |
| 171 | + fn without_hid_duplicates(&self, hid: &[HidDevice]) -> Vec<NfcDevice>; |
| 172 | +} |
| 173 | + |
| 174 | +impl NfcDeviceSliceExt for [NfcDevice] { |
| 175 | + fn without_hid_duplicates(&self, hid: &[HidDevice]) -> Vec<NfcDevice> { |
| 176 | + let hid_ids: HashSet<UsbDeviceId> = |
| 177 | + hid.iter().filter_map(HidDevice::usb_device_id).collect(); |
| 178 | + let nfc_ids: Vec<Option<UsbDeviceId>> = self.iter().map(NfcDevice::usb_device_id).collect(); |
| 179 | + self.iter() |
| 180 | + .zip(dedup_keep_mask(&nfc_ids, &hid_ids)) |
| 181 | + .filter(|&(_, keep)| keep) |
| 182 | + .map(|(device, _)| device.clone()) |
| 183 | + .collect() |
| 184 | + } |
| 185 | +} |
| 186 | + |
| 187 | +/// Pure dedup core, unit-testable without hardware. |
| 188 | +fn dedup_keep_mask(nfc_ids: &[Option<UsbDeviceId>], hid_ids: &HashSet<UsbDeviceId>) -> Vec<bool> { |
| 189 | + nfc_ids |
| 190 | + .iter() |
| 191 | + .map(|id| match id { |
| 192 | + Some(id) => !hid_ids.contains(id), |
| 193 | + None => true, |
| 194 | + }) |
| 195 | + .collect() |
| 196 | +} |
| 197 | + |
| 198 | +#[cfg(test)] |
| 199 | +mod tests { |
| 200 | + use super::*; |
| 201 | + |
| 202 | + #[test] |
| 203 | + fn dedup_keeps_non_duplicates_and_unknown() { |
| 204 | + let a = UsbDeviceId { bus: 1, address: 8 }; |
| 205 | + let b = UsbDeviceId { bus: 1, address: 9 }; |
| 206 | + let nfc_ids = [Some(a), Some(b), None]; |
| 207 | + let hid_ids: HashSet<UsbDeviceId> = [a].into_iter().collect(); |
| 208 | + |
| 209 | + assert_eq!(dedup_keep_mask(&nfc_ids, &hid_ids), vec![false, true, true]); |
| 210 | + } |
| 211 | +} |
0 commit comments