diff --git a/Cargo.toml b/Cargo.toml index 4161d22..dac4d14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,9 @@ description = "A crate to enumerate PCI devices on desktop operating systems and [dev-dependencies] paste = "1.0" +[target.'cfg(target_os = "freebsd")'.dependencies] +libc = "0.2" + [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.9.4" diff --git a/README.md b/README.md index 664cd59..85ab35a 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ Properties provided for PCI devices varies among enumerators. Enumerator | Platforms | PCI id | PCI location | Revision | Device class | PCI subsystem | Assigned IRQ | OS driver ---------------------------------------------- | --------- | ------ | ------------ | -------- | ------------ | ----------------- | ------------ | ---------- +FreeBsdDevPciEnumerator | FreeBSD | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ LinuxProcFsPciEnumerator(Fastest) | Linux | ✅ | ✅2 | ❌ | ❌ | ❌ | ✅ | ✅ LinuxProcFsPciEnumerator(HeadersOnly) | Linux | ✅ | ✅2 | ✅ | ✅ | ✅ | ❌ | ❌ LinuxProcFsPciEnumerator(SkipNoncommonHeaders) | Linux | ✅ | ✅2 | ✅ | ✅ | ❌ | ✅ | ✅ @@ -175,3 +176,6 @@ Crate feature | Default | Description ### 0.1.0 First version published with basic enumerators for Linux, Windows and MacOS. +### Not yet released +- Added PCI enumerator for FreeBSD + diff --git a/src/enumerators/freebsd/mod.rs b/src/enumerators/freebsd/mod.rs new file mode 100644 index 0000000..c70e240 --- /dev/null +++ b/src/enumerators/freebsd/mod.rs @@ -0,0 +1,16 @@ +#[cfg(target_os = "freebsd")] +mod pcidev; + +use crate::{PciEnumerator, PciInfo, PciInfoError}; + +/// A PCI Enumerator for FreeBSD that uses `ioctl(..., PCIOCGETCONF, ...)` +/// operations over `/dev/pci` to extract partial PCI information +pub struct FreeBsdDevPciEnumerator; + +impl PciEnumerator for FreeBsdDevPciEnumerator { + fn enumerate_pci(self) -> Result { + unsafe { pcidev::enumerate_devices() } + } +} + +test_enumerator!(FreeBsdDevPciEnumerator, FreeBsdDevPciEnumerator); diff --git a/src/enumerators/freebsd/pcidev.rs b/src/enumerators/freebsd/pcidev.rs new file mode 100644 index 0000000..5d3d3b3 --- /dev/null +++ b/src/enumerators/freebsd/pcidev.rs @@ -0,0 +1,182 @@ +// See https://man.freebsd.org/cgi/man.cgi?query=pci&sektion=4&manpath=freebsd-release-ports + +use std::{ffi::CStr, fs::File, io, os::fd::AsRawFd}; + +use crate::{ + pci_device::PciDeviceProperties, pci_property_result::PropertyResult, PciDevice, + PciDeviceEnumerationError, PciDeviceEnumerationErrorImpact, PciEnumerator, PciInfo, + PciInfoError, PciLocation, +}; + +// The ioctl must be restarted multiple times in case it returns +// PCI_GETCONF_LIST_CHANGED. We restart a maximum of 5 (or whatever +// its written here) times. +const MAX_CHANGED_LOOPS: u32 = 5; + +const PCIOCGETCONF: libc::c_ulong = 0xC0307005; + +const MAX_NAME_LEN: usize = 16; + +type PciGetConfStatus = u32; +const PCI_GETCONF_LAST_DEVICE: PciGetConfStatus = 0; +const PCI_GETCONF_LIST_CHANGED: PciGetConfStatus = 1; +const PCI_GETCONF_MORE_DEVS: PciGetConfStatus = 2; +const PCI_GETCONF_ERROR: PciGetConfStatus = 3; + +#[allow(dead_code)] +#[repr(C)] +struct PciConfIo { + pat_buf_len: u32, + num_patterns: u32, + patterns: *const libc::c_void, + match_buf_len: u32, + num_matches: u32, + matches: *mut PciConf, + offset: u32, + generation: u32, + status: PciGetConfStatus, +} + +#[derive(Default)] +#[repr(C)] +struct PciSel { + pc_domain: u32, + pc_bus: u8, + pc_dev: u8, + pc_func: u8, +} + +#[derive(Default)] +#[repr(C)] +struct PciConf { + pc_sel: PciSel, + pc_hdr: u8, + pc_subvendor: u16, + pc_subdevice: u16, + pc_vendor: u16, + pc_device: u16, + pc_class: u8, + pc_subclass: u8, + pc_progif: u8, + pc_revid: u8, + pd_name: [u8; MAX_NAME_LEN + 1], + pd_unit: libc::c_long, +} + +pub(super) unsafe fn enumerate_devices() -> Result { + let file = File::open("/dev/pci")?; + let devpci_file = file.as_raw_fd(); + + for _ in 0..MAX_CHANGED_LOOPS { + let mut offset = 0; + let mut pci_info = PciInfo::empty(); + let mut dev = PciConf::default(); + + // not documented (afaik) but the API expects the object not to + // move in memory, so we init it outside the inner loop + let mut pc = PciConfIo { + pat_buf_len: 0, + num_patterns: 0, + patterns: std::ptr::null(), + match_buf_len: std::mem::size_of::() as u32, + num_matches: 1, + matches: &mut dev as *mut _, + offset: 0, + generation: 0, + status: PCI_GETCONF_LAST_DEVICE, + }; + + loop { + match libc::ioctl(devpci_file, PCIOCGETCONF, &mut pc as *mut _) { + libc::EBADF | libc::ENOTTY | libc::EFAULT => { + return Err(PciInfoError::IoError(Box::new(io::ErrorKind::InvalidData))) + } + libc::EINVAL => { + return Err(PciInfoError::IoError(Box::new(io::ErrorKind::Unsupported))) + } + err if err < 0 => { + return Err(PciInfoError::IoError(Box::new(io::ErrorKind::Other))) + } + _ => (), + } + + if pc.status == PCI_GETCONF_LIST_CHANGED { + break; + } + + if pc.status == PCI_GETCONF_ERROR { + return Err(PciInfoError::EnumerationInterrupted( + "enumeration interrupted with PCI_GETCONF_ERROR".into(), + )); + } + + if pc.num_matches == 0 { + if pci_info.results.is_empty() { + return Err(PciInfoError::EnumerationInterrupted( + "enumeration interrupted with no matches".into(), + )); + } else { + pci_info.push_error(PciDeviceEnumerationError::new( + PciDeviceEnumerationErrorImpact::Bus, + PciInfoError::EnumerationInterrupted( + "enumeration interrupted with no matches".into(), + ), + )) + } + } + + if dev.pc_vendor == 0 { + pci_info.push_error(PciDeviceEnumerationError::new( + PciDeviceEnumerationErrorImpact::Device, + PciInfoError::ValueNotFound(Some("pc_vendor".into())), + )); + } else if dev.pc_device == 0 { + pci_info.push_error(PciDeviceEnumerationError::new( + PciDeviceEnumerationErrorImpact::Device, + PciInfoError::ValueNotFound(Some("pc_device".into())), + )); + } else { + let (sub_v, sub_d) = if dev.pc_subvendor != 0 && dev.pc_subdevice != 0 { + (Some(dev.pc_subvendor), Some(dev.pc_subdevice)) + } else { + (None, None) + }; + + dev.pd_name[MAX_NAME_LEN] = 0; + let name = CStr::from_bytes_until_nul(&dev.pd_name).unwrap(); + let name = name.to_string_lossy().into_owned(); + + pci_info.push_device(PciDevice::new( + dev.pc_vendor, + dev.pc_device, + PciDeviceProperties { + location: PropertyResult::with_res(PciLocation::with_segment( + (dev.pc_sel.pc_domain & 0xFFFF) as u16, + dev.pc_sel.pc_bus, + dev.pc_sel.pc_dev, + dev.pc_sel.pc_func, + )), + + subsystem_vendor_id: PropertyResult::with_val(sub_v), + subsystem_device_id: PropertyResult::with_val(sub_d), + revision: PropertyResult::with_val(dev.pc_revid), + device_class: PropertyResult::with_val(dev.pc_class), + device_subclass: PropertyResult::with_val(dev.pc_subclass), + device_iface: PropertyResult::with_val(dev.pc_progif), + os_driver: PropertyResult::with_val(Some(name)), + ..Default::default() + }, + )); + } + + offset += pc.num_matches; + pc.offset = offset; + + if pc.status != PCI_GETCONF_MORE_DEVS { + return Ok(pci_info); + } + } + } + + Err(PciInfoError::DevicesChangedTooManyTimes) +} diff --git a/src/enumerators/mod.rs b/src/enumerators/mod.rs index 254362d..8d77af6 100644 --- a/src/enumerators/mod.rs +++ b/src/enumerators/mod.rs @@ -6,6 +6,7 @@ //! //! Enumerator | Platforms | PCI id | PCI location | Revision | Device class | PCI subsystem | Assigned IRQ | OS driver //! ---------------------------------------------- | --------- | ------ | ------------ | -------- | ------------ | ----------------- | ------------ | ---------- +//! [`FreeBsdDevPciEnumerator`] | FreeBSD | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ //! [`LinuxProcFsPciEnumerator::Fastest`] | Linux | ✅ | ✅2 | ❌ | ❌ | ❌ | ✅ | ✅ //! [`LinuxProcFsPciEnumerator::HeadersOnly`] | Linux | ✅ | ✅2 | ✅ | ✅ | ✅ | ❌ | ❌ //! [`LinuxProcFsPciEnumerator::SkipNoncommonHeaders`] | Linux | ✅ | ✅2 | ✅ | ✅ | ❌ | ✅ | ✅ @@ -36,6 +37,11 @@ mod linux; #[cfg(any(doc, target_os = "linux"))] pub use linux::*; +#[cfg(any(doc, target_os = "freebsd"))] +mod freebsd; +#[cfg(any(doc, target_os = "freebsd"))] +pub use freebsd::*; + /// A trait that is implemented by all types able to enumerate PCI /// devices. pub trait PciEnumerator { @@ -52,10 +58,18 @@ pub fn default_pci_enumerator() -> Result { #[cfg(target_os = "macos")] return Ok(MacOsIoKitPciEnumerator); + #[cfg(target_os = "freebsd")] + return Ok(FreeBsdDevPciEnumerator); + #[cfg(target_os = "linux")] return Ok(LinuxProcFsPciEnumerator::Exhaustive); - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + #[cfg(not(any( + target_os = "macos", + target_os = "linux", + target_os = "windows", + target_os = "freebsd" + )))] Err::(PciInfoError::NoDefaultPciEnumeratorForPlatform) } diff --git a/src/error/pci_info_error.rs b/src/error/pci_info_error.rs index 976ae4a..e6017ab 100644 --- a/src/error/pci_info_error.rs +++ b/src/error/pci_info_error.rs @@ -11,6 +11,10 @@ pub enum PciInfoError { /// proper implementation for a default enumerator (yet). NoDefaultPciEnumeratorForPlatform, + /// The enumeration has been interrupted by an otherwise non + /// specified error. + EnumerationInterrupted(PciInfoErrorString), + /// An error occurred parsing data. For example it may trigger if /// an invalid string like "hello" was found where an hexadecimal /// number was expected. The [`PciInfoErrorString`] contained in the @@ -67,6 +71,10 @@ pub enum PciInfoError { /// Parsing of an unknown PCI specialized header type has been /// attempted. UnknownPciHeaderType(u8), + + /// The enumeration has been retried because the device list changed + /// and the maximum number of iterations has been exceeded. + DevicesChangedTooManyTimes, } impl Display for PciInfoError { @@ -97,6 +105,10 @@ impl Display for PciInfoError { write!(f, "this platform does not support a default PCI enumerator") } UnknownPciHeaderType(h) => write!(f, "unknown PCI header type 0x{h:02X}"), + EnumerationInterrupted(e) => write!(f, "the enumeration has been interrupted: {e}"), + DevicesChangedTooManyTimes => { + write!(f, "the list of PCI devices changed too many times") + } } } } diff --git a/tools/check_readyness.sh b/tools/check_readyness.sh index 4df3120..623ba32 100755 --- a/tools/check_readyness.sh +++ b/tools/check_readyness.sh @@ -11,10 +11,13 @@ TARGETS=( \ 'x86_64-pc-windows-gnu' \ 'x86_64-unknown-linux-gnu' \ 'x86_64-unknown-linux-musl' \ +'x86_64-unknown-freebsd' \ 'aarch64-apple-darwin' \ 'aarch64-unknown-linux-gnu' \ 'i686-pc-windows-gnu' \ 'i686-unknown-linux-gnu' \ +'i686-unknown-linux-gnu' \ +'i686-unknown-freebsd' \ ) export RUSTFLAGS="-D warnings"