From 37e8fef182b825b1f7ae5f00b989c0f19df790c2 Mon Sep 17 00:00:00 2001 From: John Whittington Date: Mon, 14 Nov 2022 16:45:46 +0100 Subject: [PATCH] filters working; verbose and lsusb tree content todo --- Cargo.lock | 127 ++++++++++++- Cargo.toml | 4 +- src/main.rs | 143 ++++++++++++-- src/system_profiler.rs | 414 ++++++++++++++++++++++++++++++++++++----- 4 files changed, 622 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a72647a..1c285669 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.0.22" @@ -69,13 +75,15 @@ dependencies = [ [[package]] name = "cyme" -version = "0.1.0" +version = "0.1.1" dependencies = [ "clap", "colored", + "log", "pad", "serde", "serde_json", + "simple_logger", ] [[package]] @@ -111,6 +119,24 @@ version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.16.0" @@ -211,6 +237,19 @@ dependencies = [ "serde", ] +[[package]] +name = "simple_logger" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e190a521c2044948158666916d9e872cbb9984f755e9bb3b5b75a836205affcd" +dependencies = [ + "atty", + "colored", + "log", + "time", + "windows-sys", +] + [[package]] name = "strsim" version = "0.10.0" @@ -237,6 +276,35 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "time" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +dependencies = [ + "itoa", + "libc", + "num_threads", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] + [[package]] name = "unicode-ident" version = "1.0.5" @@ -285,3 +353,60 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" diff --git a/Cargo.toml b/Cargo.toml index 793d044a..653458fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,13 +3,15 @@ name = "cyme" authors = ["John Whittington "] description = "Port of lsusb to macOS using system_profiler output" license = "GPL-3.0-or-later" -version = "0.1.0" +version = "0.1.1" edition = "2021" categories = ["command-line-utilities"] [dependencies] clap = { version = "4.0.22", features = ["derive"] } colored = "2.0.0" +log = "0.4.17" pad = "0.1.6" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.87" +simple_logger = "4.0.0" diff --git a/src/main.rs b/src/main.rs index eb68aff7..c6db24f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,8 @@ use clap::Parser; use std::env; +use colored::*; +use std::io::{Error, ErrorKind}; +use simple_logger::SimpleLogger; mod system_profiler; @@ -14,7 +17,7 @@ struct Args { #[arg(short, long, default_value_t = false)] no_colour: bool, - /// Classic dump the physical USB device hierarchy as a tree + /// Classic dump the physical USB device hierarchy as a tree - currently styling is the same but content is not #[arg(short = 't', long, default_value_t = false)] lsusb_tree: bool, @@ -23,38 +26,148 @@ struct Args { tree: bool, /// Show only devices with the specified vendor and product ID numbers (in hexadecimal) in format VID:[PID] - #[arg(short, long)] - device: Option, + #[arg(short = 'd', long)] + vidpid: Option, /// Show only devices with specified device and/or bus numbers (in decimal) in format [[bus]:][devnum] #[arg(short, long)] show: Option, - /// Increase verbosity (show descriptors) - #[arg(short, long, default_value_t = false)] - verbose: bool, + /// Increase verbosity (show descriptors) TODO + // #[arg(short, long, default_value_t = false)] + // verbose: bool, + + /// Turn debugging information on + #[arg(short = 'D', long, action = clap::ArgAction::Count)] + debug: u8, +} + +macro_rules! eprintexit { + ($error:expr) => { + // `stringify!` will convert the expression *as it is* into a string. + eprintln!("{}", $error.to_string().bold().red()); + std::process::exit(1); + }; +} + +fn parse_vidpid(s: &str) -> (Option, Option) { + if s.contains(":") { + let vid_split: Vec<&str> = s.split(":").collect(); + let vid: Option = vid_split.first() + .filter(|v| v.len() > 0) + .map_or(None, |v| u32::from_str_radix(v.trim().trim_start_matches("0x"), 16).map(|v| Some(v as u16)).unwrap_or(None)); + let pid: Option = vid_split.last() + .filter(|v| v.len() > 0) + .map_or(None, |v| u32::from_str_radix(v.trim().trim_start_matches("0x"), 16).map(|v| Some(v as u16)).unwrap_or(None)); + + (vid, pid) + } else { + let vid: Option = u32::from_str_radix(s.trim().trim_start_matches("0x"), 16).map(|v| Some(v as u16)).unwrap_or(None); + + (vid, None) + } +} + +fn parse_show(s: &str) -> Result<(Option, Option), Error> { + if s.contains(":") { + let split: Vec<&str> = s.split(":").collect(); + // TODO this unwrap should return as the result but I struggle with all this chaining... + let bus: Option = split.first() + .filter(|v| v.len() > 0) + .map_or(None, |v| v.parse::().map(Some).map_err(|e| Error::new(ErrorKind::Other, e)).unwrap_or(None)); + let device = split.last() + .filter(|v| v.len() > 0) + .map_or(None, |v| v.parse::().map(Some).map_err(|e| Error::new(ErrorKind::Other, e)).unwrap_or(None)); + + Ok((bus, device)) + } else { + let device: Option = s.trim().parse::().map(Some).map_err(|e| Error::new(ErrorKind::Other, e)).unwrap_or(None); + + Ok((None, device)) + } + } fn main() { let args = Args::parse(); - let sp_usb = system_profiler::get_spusb().unwrap(); + + match args.debug { + 0 => (), + 1 => SimpleLogger::new() + .with_utc_timestamps() + .with_level(log::Level::Info.to_level_filter()) + .init() + .unwrap(), + 2 => SimpleLogger::new() + .with_utc_timestamps() + .with_level(log::Level::Debug.to_level_filter()) + .init() + .unwrap(), + 3 | _ => SimpleLogger::new() + .with_utc_timestamps() + .with_level(log::Level::Trace.to_level_filter()) + .init() + .unwrap(), + } // just set the env for this process if args.no_colour { env::set_var("NO_COLOR", "1"); } - if args.lsusb { - if args.lsusb_tree { - print!("{:+}", sp_usb); - } else { - print!("{:}", sp_usb); + let sp_usb = system_profiler::get_spusb().unwrap_or_else(|e| { + eprintexit!(std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to parse system_profiler output: {}", e))); + }); + + log::debug!("{:#?}", sp_usb); + + let mut filter = system_profiler::USBFilter { + vid: None, + pid: None, + bus: None, + port: None, + }; + + if let Some(vidpid) = &args.vidpid { + let (vid, pid) = parse_vidpid(&vidpid.as_str()); + filter.vid = vid; + filter.pid = pid; + } + + if let Some(show) = &args.show { + let (bus, port) = parse_show(&show.as_str()).unwrap_or_else(|e| { + eprintexit!(Error::new(ErrorKind::Other, format!("Failed to parse show parameter: {}", e))); + }); + filter.bus = bus; + filter.port = port; + } + + log::info!("{:?}", filter); + + if args.vidpid.is_some() || args.show.is_some() { + let mut devs = sp_usb.get_all_devices(); + devs = filter.filter_devices_ref(devs); + for d in devs { + if args.lsusb { + println!("{:}", d); + } else { + println!("{:#}", d); + } } } else { - if args.tree { - print!("{:+#}", sp_usb); + if args.lsusb { + if args.lsusb_tree { + eprintln!("lsusb tree is styling only; content is not the same!"); + print!("{:+}", sp_usb); + } else { + print!("{:}", sp_usb); + } } else { - print!("{:#}", sp_usb); + if args.tree { + print!("{:+#}", sp_usb); + } else { + print!("{:#}", sp_usb); + } } } } diff --git a/src/system_profiler.rs b/src/system_profiler.rs index 85773588..be7341a1 100644 --- a/src/system_profiler.rs +++ b/src/system_profiler.rs @@ -1,18 +1,20 @@ -use std::fmt; /// Parser for macOS `system_profiler` command -json output with SPUSBDataType. /// /// USBBus and USBDevice structs are used as deserializers for serde. The JSON output with the -json flag is not really JSON; all values are String regardless of contained data so it requires some extra work. Additionally, some values differ slightly from the non json output such as the speed - it is a description rather than numerical. /// /// J.Whittington - 2022 +use std::fmt; use std::io; use std::str::FromStr; use colored::*; use serde::{Deserialize, Deserializer, Serialize}; +use serde::de::{self, Visitor}; use std::process::Command; /// borrowed from https://github.com/vityafx/serde-aux/blob/master/src/field_attributes.rs with addition of base16 encoding /// Deserializes an option number from string or a number. +/// Only really used for vendor id and product id so TODO make struct for these fn deserialize_option_number_from_string<'de, T, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, @@ -32,14 +34,14 @@ where "" => Ok(None), _ => { // -json returns apple_vendor_id in vendor_id for some reason not base16 like normal - if s == "apple_vendor_id" { + if s.contains("apple_vendor_id") { s = "0x05ac"; } // the vendor_id can be appended with manufacturer name for some reason...split with space to get just base16 encoding let vendor_vec: Vec<&str> = s.split(" ").collect(); let removed_0x = vendor_vec[0].trim_start_matches("0x"); - if removed_0x != s { + if removed_0x != vendor_vec[0] { let base16_num = u64::from_str_radix(removed_0x.trim(), 16); let result = match base16_num { Ok(num) => T::from_str(num.to_string().as_str()), @@ -61,7 +63,18 @@ where #[derive(Debug, Serialize, Deserialize)] pub struct SPUSBDataType { #[serde(rename(deserialize = "SPUSBDataType"))] - buses: Vec, + pub buses: Vec, +} + +impl SPUSBDataType { + pub fn get_all_devices<'a>(&'a self) -> Vec<&'a USBDevice> { + let mut ret = Vec::new(); + for bus in &self.buses { + ret.append(&mut bus.get_all_devices()); + } + + ret + } } impl fmt::Display for SPUSBDataType { @@ -83,7 +96,7 @@ impl fmt::Display for SPUSBDataType { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct USBBus { #[serde(rename(deserialize = "_name"))] name: String, @@ -101,6 +114,30 @@ pub struct USBBus { devices: Option>, } +impl USBBus { + pub fn get_all_devices<'a>(&'a self) -> Vec<&'a USBDevice> { + if let Some(devices) = &self.devices { + get_all_devices(&devices) + } else { + Vec::new() + } + } +} + +pub fn get_all_devices(devices: &Vec) -> Vec<&USBDevice> { + let mut ret: Vec<&USBDevice> = Vec::new(); + for device in devices { + // push each device into pointer array + ret.push(device); + // and run recursively for the device if it has some + if let Some(d) = &device.devices { + ret.append(&mut get_all_devices(&d)) + } + } + + return ret; +} + pub fn write_devices_recursive(f: &mut fmt::Formatter, devices: &Vec) -> fmt::Result { for device in devices { // print the device details @@ -113,7 +150,7 @@ pub fn write_devices_recursive(f: &mut fmt::Formatter, devices: &Vec) } else if f.sign_plus() { writeln!(f, "{:+}", device)?; } else { - writeln!(f, "{:}", device)?; + writeln!(f, "{}", device)?; } // print all devices with this device - if hub for example device @@ -175,7 +212,266 @@ impl fmt::Display for USBBus { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize)] +/// location_id String from system_profiler is "LocationReg / Port" +/// The LocationReg has the tree structure (0xbbdddddd): +/// 0x -- always +/// bb -- bus number in hexadecimal +/// dddddd -- up to six levels for the tree, each digit represents its +/// position on that level +struct DeviceLocation { + bus: u8, + tree_positions: Vec, + port: Option, +} + +impl FromStr for DeviceLocation { + type Err = io::Error; + + fn from_str(s: &str) -> Result { + let location_split: Vec<&str> = s.split("/").collect(); + let reg = location_split + .first() + .unwrap() + .trim() + .trim_start_matches("0x"); + + // get position in tree based on number of non-zero chars or just 0 if not using tree + let tree_positions = reg.get(2..).unwrap_or("0").trim_end_matches("0") + .chars().map(|v| v.to_digit(10).unwrap_or(0) as u8).collect(); + // bus no is msb + let bus = (u32::from_str_radix(®, 16).map_err(|v| io::Error::new(io::ErrorKind::Other, v)).unwrap() >> 24) as u8; + // port is after / but not always present + let port = match location_split + .last() + .unwrap() + .trim() + .parse::() { + Ok(v) => Some(v), + Err(_) => None, + }; + + Ok(DeviceLocation { bus, tree_positions, port }) + } +} + +impl<'de> Deserialize<'de> for DeviceLocation { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct DeviceLocationVisitor; + + impl<'de> Visitor<'de> for DeviceLocationVisitor { + type Value = DeviceLocation; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string representation of speed") + } + + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + DeviceLocation::from_str(value.as_str()).map_err(serde::de::Error::custom) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + DeviceLocation::from_str(value).map_err(serde::de::Error::custom) + } + } + + deserializer.deserialize_any(DeviceLocationVisitor) + } +} + + +#[derive(Debug, Clone, PartialEq, Serialize)] +struct DeviceNumericalUnit { + value: T, + unit: String, + description: Option, +} + +impl fmt::Display for DeviceNumericalUnit { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:} {:}", self.value, self.unit) + } +} + +impl fmt::Display for DeviceNumericalUnit { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(precision) = f.precision() { + // If we received a precision, we use it. + write!(f, "{1:.*} {2}", precision, self.value, self.unit) + } else { + // Otherwise we default to 2. + write!(f, "{:.2} {}", self.value, self.unit) + } + } +} + +impl FromStr for DeviceNumericalUnit { + type Err = io::Error; + + fn from_str(s: &str) -> Result { + let value_split: Vec<&str> = s.trim().split(' ').collect(); + if value_split.len() >= 2 { + Ok(DeviceNumericalUnit { + value: value_split[0].trim().parse::().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?, + unit: value_split[1].trim().to_string(), + description: None, + }) + } else { + Err(io::Error::new(io::ErrorKind::Other, "string split does not contain [u32] [unit]")) + } + } +} + +impl FromStr for DeviceNumericalUnit { + type Err = io::Error; + + fn from_str(s: &str) -> Result { + let value_split: Vec<&str> = s.trim().split(' ').collect(); + if value_split.len() >= 2 { + Ok(DeviceNumericalUnit { + value: value_split[0].trim().parse::().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?, + unit: value_split[1].trim().to_string(), + description: None, + }) + } else { + Err(io::Error::new(io::ErrorKind::Other, "string split does not contain [f32] [unit]")) + } + } +} + + +//TODO these should be generic but not sure how to pass generic to visitor? +impl<'de> Deserialize<'de> for DeviceNumericalUnit { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + + struct DeviceNumericalUnitU32Visitor; + + impl<'de> Visitor<'de> for DeviceNumericalUnitU32Visitor { + type Value = DeviceNumericalUnit; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string with format '[int] [unit]'") + } + + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + Ok(DeviceNumericalUnit::from_str(value.as_str()).map_err(E::custom)?) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(DeviceNumericalUnit::from_str(value).map_err(E::custom)?) + } + } + + deserializer.deserialize_str(DeviceNumericalUnitU32Visitor) + } +} + +impl<'de> Deserialize<'de> for DeviceNumericalUnit { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + + struct DeviceNumericalUnitF32Visitor; + + impl<'de> Visitor<'de> for DeviceNumericalUnitF32Visitor { + type Value = DeviceNumericalUnit; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string with format '[float] [unit]'") + } + + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + Ok(DeviceNumericalUnit::from_str(value.as_str()).map_err(E::custom)?) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(DeviceNumericalUnit::from_str(value).map_err(E::custom)?) + } + } + + deserializer.deserialize_str(DeviceNumericalUnitF32Visitor) + } +} + +// TODO this could probably convert to Enun of SuperSpeedPlus, FullSpeed etc so that serde auto deserialize enum then use TryInto DeviceNumericalUnit for enum value +#[derive(Debug, Clone, PartialEq, Serialize)] +enum DeviceSpeed { + NumericalUnit(DeviceNumericalUnit), + Description(String), +} + +impl fmt::Display for DeviceSpeed { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + DeviceSpeed::NumericalUnit(v) => write!(f, "{:.1}", v), + DeviceSpeed::Description(v) => write!(f, "{}", v), + } + } +} + +impl<'de> Deserialize<'de> for DeviceSpeed { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct DeviceSpeedVisitor; + + impl<'de> Visitor<'de> for DeviceSpeedVisitor { + type Value = DeviceSpeed; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string representation of speed") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + // TODO + // value.try_into(DeviceNumericalUnit); + + match value.trim() { + "super_speed_plus" => Ok(DeviceSpeed::NumericalUnit(DeviceNumericalUnit{value: 20.0, unit: String::from("Gb/s"), description: Some(value.to_owned())})), + "super_speed" => Ok(DeviceSpeed::NumericalUnit(DeviceNumericalUnit{value: 5.0, unit: String::from("Gb/s"), description: Some(value.to_owned())})), + "high_speed"|"high_bandwidth" => Ok(DeviceSpeed::NumericalUnit(DeviceNumericalUnit{value: 480.0, unit: String::from("Mb/s"), description: Some(value.to_owned())})), + "full_speed" => Ok(DeviceSpeed::NumericalUnit(DeviceNumericalUnit{value: 12.0, unit: String::from("Mb/s"), description: Some(value.to_owned())})), + "low_speed" => Ok(DeviceSpeed::NumericalUnit(DeviceNumericalUnit{value: 1.5, unit: String::from("Mb/s"), description: Some(value.to_owned())})), + v => Ok(DeviceSpeed::Description(v.to_string())), + } + } + } + + deserializer.deserialize_any(DeviceSpeedVisitor) + } +} + + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct USBDevice { #[serde(rename(deserialize = "_name"))] name: String, @@ -183,7 +479,7 @@ pub struct USBDevice { vendor_id: Option, #[serde(default, deserialize_with = "deserialize_option_number_from_string")] product_id: Option, - location_id: String, + location_id: DeviceLocation, serial_num: Option, manufacturer: Option, #[serde(default, deserialize_with = "deserialize_option_number_from_string")] @@ -192,7 +488,7 @@ pub struct USBDevice { bus_power: Option, #[serde(default, deserialize_with = "deserialize_option_number_from_string")] bus_power_used: Option, - device_speed: Option, + device_speed: Option, #[serde(default, deserialize_with = "deserialize_option_number_from_string")] extra_current_used: Option, // devices can be hub and have devices attached @@ -200,43 +496,63 @@ pub struct USBDevice { devices: Option>, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct USBFilter { + pub vid: Option, + pub pid: Option, + pub bus: Option, + pub port: Option, +} + +impl USBFilter { + // pub fn retain_buses(self, buses: &mut Vec) -> () { + // buses.retain(|b| b.usb_bus_number == self.bus || self.bus.is_none() || b.usb_bus_number.is_none()); + + // for bus in buses { + // bus.devices.as_mut().map_or((), |d| self.retain_devices(d)); + // } + // } + + // pub fn retain_devices(self, devices: &mut Vec) -> () { + // *devices = devices + // .iter() + // .filter(|d| Some(d.location_id.bus) == self.bus || self.bus.is_none()) + // .filter(|d| d.location_id.port == self.port || self.port.is_none()) + // .filter(|d| d.vendor_id == self.vid || self.vid.is_none()) + // .filter(|d| d.product_id == self.pid || self.pid.is_none()) + // .map(|d| d.to_owned()) + // .collect::>(); + + // for d in devices { + // d.devices.as_mut().map_or((), |d| self.retain_devices(d)); + // } + // } + + pub fn filter_devices_ref(self, devices: Vec<&USBDevice>) -> Vec<&USBDevice> { + let new: Vec<&USBDevice> = devices + .into_iter() + .filter(|d| Some(d.location_id.bus) == self.bus || self.bus.is_none()) + .filter(|d| d.location_id.port == self.port || self.port.is_none()) + .filter(|d| d.vendor_id == self.vid || self.vid.is_none()) + .filter(|d| d.product_id == self.pid || self.pid.is_none()) + .collect(); + + new + } +} + impl fmt::Display for USBDevice { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // # Build a formatted line to be sorted for the tree. - // # The LocationID has the tree structure (0xbbdddddd): - // # 0x -- always - // # bb -- bus number in hexadecimal - // # dddddd -- up to six levels for the tree, each digit represents its - // # position on that level - // location_id is "location_reg / port" - let location_split: Vec<&str> = self.location_id.split("/").collect(); - let reg = location_split - .first() - .unwrap_or(&"0x00000000") - .trim() - .trim_start_matches("0x"); - let device_no = location_split - .last() - .unwrap_or(&"0") - .trim() - .parse::() - .unwrap_or(1); - // bus no is msb - let bus_no = u32::from_str_radix(®, 16).unwrap_or(0) >> 24; - // get position in tree based on number of non-zero chars or just 0 if not using tree let mut spaces = if f.sign_plus() { - reg.get(2..).unwrap_or("0").trim_end_matches("0").len() * 4 + self.location_id.tree_positions.len() * 4 } else { 0 }; // map speed from text back to data rate if tree - let speed = match self.device_speed.as_ref().map(|s| s.as_str()) { - Some("super_speed") => "5 Gb/s", - Some("full_speed") => "12 Mb/s", - Some("high_speed") => "480 Mb/s", - Some(x) => x, - None => "", + let speed = match &self.device_speed { + Some(v) => v.to_string(), + None => String::from(""), }; // tree chars to prepend if plus formatted @@ -255,13 +571,13 @@ impl fmt::Display for USBDevice { if f.alternate() { write!( f, - "{:>spaces$}Bus {:} Device {:} ID {:}:{:} {:} {:} {:}", + "{:>spaces$}{}/{} {}:{} {} {} {}", tree.bright_black(), - format!("{:03}", bus_no).cyan(), - format!("{:03}", device_no).magenta(), - format!("{:04x}", self.vendor_id.unwrap()).yellow().bold(), - format!("{:04x}", self.product_id.unwrap()).yellow(), - self.name.trim().blue(), + format!("{:03}", self.location_id.bus).cyan(), + format!("{:03}", self.location_id.port.unwrap_or(0)).magenta(), + format!("0x{:04x}", self.vendor_id.unwrap()).yellow().bold(), + format!("0x{:04x}", self.product_id.unwrap()).yellow(), + self.name.trim().bold().blue(), self.serial_num .as_ref() .unwrap_or(&String::from("None")) @@ -277,10 +593,10 @@ impl fmt::Display for USBDevice { } write!( f, - "{:>spaces$}Bus {:03} Device {:03} ID {:04x}:{:04x} {:}", + "{:>spaces$}Bus {:03} Device {:03}: ID {:04x}:{:04x} {}", tree, - bus_no, - device_no, + self.location_id.bus, + self.location_id.port.unwrap_or(0), self.vendor_id.unwrap_or(0xffff), self.product_id.unwrap_or(0xffff), self.name.trim(), @@ -331,9 +647,9 @@ mod tests { assert_eq!(device.bcd_device, Some(1.00)); assert_eq!(device.bus_power, Some(500)); assert_eq!(device.bus_power_used, Some(500)); - assert_eq!(device.device_speed, Some("full_speed".to_string())); + assert_eq!(device.device_speed, Some(DeviceSpeed::NumericalUnit(DeviceNumericalUnit { value: 12.0, unit: String::from("Mb/s"), description: Some(String::from("full_speed")) }))); assert_eq!(device.extra_current_used, Some(0)); - assert_eq!(device.location_id, "0x02110000 / 3".to_string()); + assert_eq!(device.location_id, DeviceLocation{ bus: 2, tree_positions: vec![1, 1], port: Some(3)} ); assert_eq!(device.manufacturer, Some("Arduino LLC".to_string())); assert_eq!(device.product_id, Some(0x804d)); assert_eq!(device.vendor_id, Some(0x2341));