You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
767 lines
23 KiB
Rust
767 lines
23 KiB
Rust
use anyhow::{Context, Error};
|
|
use config::{Config, File, FileFormat, Value};
|
|
use serde::de::Visitor;
|
|
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
|
use std::collections::HashMap;
|
|
use std::fmt::{Debug, Display, Formatter};
|
|
use std::str::FromStr;
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
|
pub struct InstanceConfig {
|
|
pub name: String,
|
|
pub arch: String,
|
|
pub chipset: String,
|
|
pub kvm: bool,
|
|
pub memory: u64,
|
|
pub cpu: CpuConfig,
|
|
pub disks: Vec<DiskConfig>,
|
|
pub uefi: UefiConfig,
|
|
pub vfio: Vec<VfioConfig>,
|
|
pub looking_glass: LookingGlassConfig,
|
|
pub scream: ScreamConfig,
|
|
pub spice: SpiceConfig,
|
|
}
|
|
|
|
impl InstanceConfig {
|
|
pub fn from_toml(toml: &str) -> Result<Self, anyhow::Error> {
|
|
let toml = Config::new().with_merged(File::from_str(toml, FileFormat::Toml))?;
|
|
Self::from_config(toml)
|
|
}
|
|
|
|
pub fn from_config(config: Config) -> Result<InstanceConfig, anyhow::Error> {
|
|
let mut instance_config = InstanceConfig::default();
|
|
if let Ok(name) = config.get_str("machine.name") {
|
|
instance_config.name = name
|
|
}
|
|
|
|
if let Ok(kvm) = config.get::<Value>("machine.kvm") {
|
|
instance_config.kvm = kvm.into_bool().context("machine.kvm should be a boolean")?;
|
|
}
|
|
|
|
if let Ok(mem) = config.get::<Value>("machine.memory") {
|
|
let mem = mem
|
|
.into_str()
|
|
.context("machine.memory should be a string or number")?;
|
|
instance_config.memory = parse_size(&mem)?;
|
|
}
|
|
|
|
if let Ok(cpu) = config.get_table("cpu") {
|
|
instance_config.cpu.apply_table(cpu)?
|
|
}
|
|
|
|
if let Ok(disks) = config.get::<Value>("disk") {
|
|
let arr = disks.into_array().context("disk should be an array")?;
|
|
for (i, disk) in arr.into_iter().enumerate() {
|
|
let table = disk
|
|
.into_table()
|
|
.with_context(|| format!("disk[{}] should be a table", i))?;
|
|
instance_config.disks.push(DiskConfig::from_table(table)?);
|
|
}
|
|
}
|
|
|
|
if let Ok(uefi) = config.get_table("uefi") {
|
|
instance_config.uefi.apply_table(uefi)?;
|
|
}
|
|
|
|
if let Ok(vfio) = config.get::<Value>("vfio") {
|
|
let arr = vfio.into_array().context("vfio should be an array")?;
|
|
for (i, disk) in arr.into_iter().enumerate() {
|
|
let table = disk
|
|
.into_table()
|
|
.with_context(|| format!("vfio[{}] should be a table", i))?;
|
|
instance_config.vfio.push(VfioConfig::from_table(table)?);
|
|
}
|
|
}
|
|
|
|
instance_config.looking_glass = LookingGlassConfig::from_table(
|
|
config.get_table("looking-glass").unwrap_or_default(),
|
|
&instance_config.name,
|
|
)?;
|
|
instance_config.scream = ScreamConfig::from_table(
|
|
config.get_table("scream").unwrap_or_default(),
|
|
&instance_config.name,
|
|
)?;
|
|
instance_config.spice =
|
|
SpiceConfig::from_table(config.get_table("spice").unwrap_or_default())?;
|
|
|
|
if let Ok(features) = config.get::<Vec<String>>("machine.features") {
|
|
for feature in features {
|
|
match feature.as_str() {
|
|
"looking-glass" => instance_config.looking_glass.enabled = true,
|
|
"spice" => instance_config.spice.enabled = true,
|
|
"scream" => instance_config.scream.enabled = true,
|
|
"uefi" => instance_config.uefi.enabled = true,
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(instance_config)
|
|
}
|
|
}
|
|
|
|
impl Default for InstanceConfig {
|
|
fn default() -> Self {
|
|
InstanceConfig {
|
|
name: "vore".to_string(),
|
|
arch: std::env::consts::ARCH.to_string(),
|
|
chipset: "q35".to_string(),
|
|
kvm: true,
|
|
// 2 GB
|
|
memory: 2 * 1024 * 1024 * 1024,
|
|
cpu: Default::default(),
|
|
disks: vec![],
|
|
uefi: Default::default(),
|
|
vfio: vec![],
|
|
looking_glass: Default::default(),
|
|
scream: Default::default(),
|
|
spice: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
|
pub struct CpuConfig {
|
|
pub amount: u64,
|
|
pub cores: u64,
|
|
pub threads: u64,
|
|
pub dies: u64,
|
|
pub sockets: u64,
|
|
}
|
|
|
|
impl Default for CpuConfig {
|
|
fn default() -> Self {
|
|
CpuConfig {
|
|
amount: 2,
|
|
cores: 1,
|
|
threads: 2,
|
|
dies: 1,
|
|
sockets: 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_positive_number_from_table(
|
|
table: &HashMap<String, Value>,
|
|
key: &str,
|
|
prefix: &str,
|
|
) -> Result<Option<u64>, Error> {
|
|
table
|
|
.get(key)
|
|
.cloned()
|
|
.map(|x| {
|
|
x.into_int()
|
|
.with_context(|| format!("Failed to parse {}.{} as number", prefix, key))
|
|
.and_then(|x| {
|
|
Some(x)
|
|
.filter(|x| !x.is_negative())
|
|
.map(|x| x as u64)
|
|
.ok_or_else(|| {
|
|
anyhow::Error::msg(format!("{}.{} can't be negative", prefix, key))
|
|
})
|
|
})
|
|
})
|
|
.transpose()
|
|
}
|
|
|
|
impl CpuConfig {
|
|
fn apply_table(&mut self, table: HashMap<String, Value>) -> Result<(), anyhow::Error> {
|
|
if let Some(amount) = get_positive_number_from_table(&table, "amount", "cpu")? {
|
|
self.amount = amount;
|
|
}
|
|
|
|
if let Some(cores) = get_positive_number_from_table(&table, "cores", "cpu")? {
|
|
self.cores = cores;
|
|
}
|
|
|
|
if let Some(threads) = get_positive_number_from_table(&table, "threads", "cpu")? {
|
|
self.threads = threads;
|
|
}
|
|
|
|
if let Some(dies) = get_positive_number_from_table(&table, "dies", "cpu")? {
|
|
self.dies = dies;
|
|
}
|
|
|
|
if let Some(sockets) = get_positive_number_from_table(&table, "sockets", "cpu")? {
|
|
self.sockets = sockets;
|
|
}
|
|
|
|
if !table.contains_key("amount") {
|
|
self.amount = self.sockets * self.dies * self.cores * self.threads;
|
|
} else {
|
|
if table
|
|
.keys()
|
|
.any(|x| ["cores", "sockets", "dies", "threads"].contains(&x.as_str()))
|
|
{
|
|
let calc_amount = self.sockets * self.dies * self.cores * self.threads;
|
|
if self.amount != calc_amount {
|
|
Err(anyhow::Error::msg(format!("Amount of cpu's ({}) from sockets ({}), dies ({}), cores ({}) and threads ({}) differs from specified ({}) cpu's", calc_amount, self.sockets, self.dies, self.cores, self.threads, self.amount)))?;
|
|
}
|
|
} else {
|
|
if (self.amount % 2) == 0 {
|
|
self.cores = self.amount / 2;
|
|
} else {
|
|
self.threads = 1;
|
|
self.cores = self.amount;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn parse_size(orig_input: &str) -> Result<u64, anyhow::Error> {
|
|
let input = orig_input.to_string().to_lowercase().replace(" ", "");
|
|
let mut input = input.strip_suffix("b").unwrap_or(&input);
|
|
let mut modifier: u64 = 1;
|
|
|
|
if input.chars().last().unwrap_or('_').is_alphabetic() {
|
|
modifier = match input.chars().last().unwrap() {
|
|
'k' => {
|
|
return Err(anyhow::Error::msg(
|
|
"size can only be specified in megabytes or larger",
|
|
));
|
|
}
|
|
'm' => 1,
|
|
'g' => 1024,
|
|
't' => 1024 * 1024,
|
|
_ => {
|
|
return Err(anyhow::Error::msg(format!(
|
|
"'{}' is not a valid size",
|
|
orig_input
|
|
)));
|
|
}
|
|
};
|
|
|
|
input = &input[..input.len() - 1];
|
|
}
|
|
|
|
if input.len() == 0 {
|
|
return Err(anyhow::Error::msg(format!(
|
|
"'{}' is not a valid size",
|
|
orig_input
|
|
)));
|
|
}
|
|
|
|
u64::from_str(input)
|
|
.context(format!("'{}' is not a valid size", orig_input))
|
|
.map(|x| x * modifier)
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
|
pub struct UefiConfig {
|
|
pub enabled: bool,
|
|
}
|
|
|
|
impl Default for UefiConfig {
|
|
fn default() -> Self {
|
|
UefiConfig { enabled: false }
|
|
}
|
|
}
|
|
|
|
impl UefiConfig {
|
|
fn apply_table(&mut self, table: HashMap<String, Value>) -> Result<(), anyhow::Error> {
|
|
if let Some(enabled) = table
|
|
.get("enabled")
|
|
.cloned()
|
|
.map(|x| x.into_bool().context("eufi.enabled should be a boolean"))
|
|
.transpose()?
|
|
{
|
|
self.enabled = enabled
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
|
pub struct ScreamConfig {
|
|
pub enabled: bool,
|
|
pub mem_path: String,
|
|
pub buffer_size: u64,
|
|
}
|
|
|
|
impl ScreamConfig {
|
|
pub fn from_table(
|
|
table: HashMap<String, Value>,
|
|
name: &str,
|
|
) -> Result<ScreamConfig, anyhow::Error> {
|
|
let mut cfg = ScreamConfig::default();
|
|
if let Some(enabled) = table.get("enabled").cloned() {
|
|
cfg.enabled = enabled.into_bool()?;
|
|
}
|
|
|
|
if let Some(mem_path) = table.get("mem-path").cloned() {
|
|
cfg.mem_path = mem_path.into_str()?;
|
|
} else {
|
|
cfg.mem_path = format!("/dev/shm/{}-scream", name);
|
|
}
|
|
|
|
if let Some(buffer_size) = table.get("buffer-size").cloned() {
|
|
cfg.buffer_size = buffer_size.into_int()? as u64;
|
|
}
|
|
|
|
Ok(cfg)
|
|
}
|
|
}
|
|
|
|
impl Default for ScreamConfig {
|
|
fn default() -> Self {
|
|
ScreamConfig {
|
|
enabled: false,
|
|
mem_path: "".to_string(),
|
|
buffer_size: 2097152,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
|
pub struct LookingGlassConfig {
|
|
pub enabled: bool,
|
|
pub mem_path: String,
|
|
pub buffer_size: u64,
|
|
pub width: u64,
|
|
pub height: u64,
|
|
pub bit_depth: u64,
|
|
}
|
|
|
|
impl Default for LookingGlassConfig {
|
|
fn default() -> Self {
|
|
LookingGlassConfig {
|
|
enabled: false,
|
|
mem_path: "".to_string(),
|
|
buffer_size: 0,
|
|
width: 1920,
|
|
height: 1080,
|
|
bit_depth: 8,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl LookingGlassConfig {
|
|
pub fn calc_buffer_size_from_screen(&mut self) {
|
|
// https://forum.level1techs.com/t/solved-what-is-max-frame-size-determined-by/170312/4
|
|
//
|
|
// required memory size is
|
|
//
|
|
// height * width * 4 * 2 + 2mb
|
|
//
|
|
// And shared memory size needs to be a power off 2
|
|
//
|
|
let mut minimum_needed =
|
|
self.width * self.height * (((self.bit_depth * 4) as f64 / 8f64).ceil() as u64);
|
|
|
|
// 2 frames
|
|
minimum_needed *= 2;
|
|
|
|
// Add additional 2mb
|
|
minimum_needed += 2 * 1024 * 1024;
|
|
|
|
let mut i = 1;
|
|
let mut buffer_size = 1;
|
|
while buffer_size < minimum_needed {
|
|
i += 1;
|
|
buffer_size = 2u64.pow(i);
|
|
}
|
|
|
|
self.buffer_size = buffer_size;
|
|
}
|
|
|
|
pub fn from_table(
|
|
table: HashMap<String, Value>,
|
|
name: &str,
|
|
) -> Result<LookingGlassConfig, anyhow::Error> {
|
|
let mut cfg = LookingGlassConfig::default();
|
|
|
|
if let Some(enabled) = table.get("enabled").cloned() {
|
|
cfg.enabled = enabled.into_bool()?;
|
|
}
|
|
|
|
if let Some(mem_path) = table.get("mem-path").cloned() {
|
|
cfg.mem_path = mem_path.into_str()?;
|
|
} else {
|
|
cfg.mem_path = format!("/dev/shm/{}/looking-glass", name);
|
|
}
|
|
|
|
match (table.get("buffer-size").cloned(), table.get("width").cloned(), table.get("height").cloned()) {
|
|
(Some(buffer_size), None, None) => {
|
|
cfg.buffer_size = buffer_size.into_int()? as u64;
|
|
}
|
|
|
|
(None, Some(width), Some(height)) => {
|
|
let width = width.into_int()? as u64;
|
|
let height = height.into_int()? as u64;
|
|
let bit_depth = table.get("bit-depth").cloned().map_or(Ok(cfg.bit_depth), |x| x.into_int().map(|x| x as u64))?;
|
|
cfg.bit_depth = bit_depth;
|
|
cfg.width = width;
|
|
cfg.height = height;
|
|
cfg.calc_buffer_size_from_screen();
|
|
}
|
|
|
|
(None, None, None) => {
|
|
cfg.calc_buffer_size_from_screen()
|
|
}
|
|
|
|
_ => anyhow::bail!("for looking-glass either width and height need to be set or buffer-size should be set")
|
|
}
|
|
|
|
Ok(cfg)
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
|
pub struct DiskConfig {
|
|
pub disk_type: String,
|
|
pub preset: String,
|
|
pub path: String,
|
|
pub read_only: bool,
|
|
}
|
|
|
|
impl DiskConfig {
|
|
pub fn from_table(table: HashMap<String, Value>) -> Result<DiskConfig, anyhow::Error> {
|
|
let path = table
|
|
.get("path")
|
|
.cloned()
|
|
.ok_or_else(|| anyhow::Error::msg("Disk needs a path"))?
|
|
.into_str()
|
|
.context("Disk path must be a string")?;
|
|
|
|
let disk_type = if let Some(disk_type) = table.get("type").cloned() {
|
|
disk_type.into_str()?
|
|
} else {
|
|
(kiam::when! {
|
|
path.starts_with("/dev") | path.ends_with(".iso") => "raw",
|
|
path.ends_with(".qcow2") => "qcow2",
|
|
_ => return Err(anyhow::Error::msg("Can't figure out from path what type of disk driver should be used"))
|
|
}).to_string()
|
|
};
|
|
|
|
let preset = table.get("preset")
|
|
.cloned()
|
|
.context("Every disk should have a preset set")?
|
|
.into_str()?;
|
|
|
|
let read_only = table.get("read-only")
|
|
.cloned()
|
|
.map(|x| x.into_bool())
|
|
.transpose()
|
|
.context("Failed to read read-only as boolean from config")?
|
|
.unwrap_or(false);
|
|
|
|
let disk = DiskConfig {
|
|
disk_type,
|
|
preset,
|
|
path,
|
|
read_only,
|
|
};
|
|
|
|
Ok(disk)
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
|
pub struct VfioConfig {
|
|
pub address: PCIAddress,
|
|
pub vendor: Option<u32>,
|
|
pub device: Option<u32>,
|
|
pub index: u32,
|
|
pub graphics: bool,
|
|
pub multifunction: bool,
|
|
}
|
|
|
|
pub fn read_pci_ids(addr: &PCIAddress) -> Result<(u32, u32), anyhow::Error> {
|
|
let device = std::fs::read_to_string(format!("/sys/bus/pci/devices/{:#}/device", addr))
|
|
.with_context(|| {
|
|
format!(
|
|
"Failed to read the device id of PCI device at {:#} ({})",
|
|
addr,
|
|
format!("/sys/bus/pci/devices/{:#}/device", addr)
|
|
)
|
|
})?;
|
|
let found_device = u32::from_str_radix(device.trim_start_matches("0x").trim_end(), 16)?;
|
|
|
|
let vendor = std::fs::read_to_string(format!("/sys/bus/pci/devices/{:#}/vendor", addr))
|
|
.with_context(|| {
|
|
format!(
|
|
"Failed to read the vendor id of PCI device at {:#} ({})",
|
|
addr,
|
|
format!("/sys/bus/pci/devices/{:#}/vendor", addr)
|
|
)
|
|
})?;
|
|
let found_vendor = u32::from_str_radix(vendor.trim_start_matches("0x").trim_end(), 16)?;
|
|
|
|
Ok((found_vendor, found_device))
|
|
}
|
|
|
|
impl VfioConfig {
|
|
pub fn from_table(table: HashMap<String, Value>) -> Result<VfioConfig, anyhow::Error> {
|
|
let mut address = table
|
|
.get("addr")
|
|
.or_else(|| table.get("address"))
|
|
.cloned()
|
|
.map(|x| PCIAddress::from_str(&x.into_str()?))
|
|
.transpose()?;
|
|
|
|
let vendor = table
|
|
.get("vendor")
|
|
.cloned()
|
|
.map(|x| x.into_int().map(|x| x as u32))
|
|
.transpose()?;
|
|
let device = table
|
|
.get("device")
|
|
.cloned()
|
|
.map(|x| x.into_int().map(|x| x as u32))
|
|
.transpose()?;
|
|
let index = table
|
|
.get("index")
|
|
.cloned()
|
|
.map(|x| x.into_int().map(|x| x as u32))
|
|
.transpose()?
|
|
.unwrap_or(0);
|
|
|
|
let address = match (address, vendor, device) {
|
|
(Some(addr), vendor, device) => {
|
|
let (found_vendor, found_device) = read_pci_ids(&addr)?;
|
|
|
|
if let Some(device) = device {
|
|
if device != found_device {
|
|
anyhow::bail!(
|
|
"VFIO expects a PCI device on address {} with the device id {:#04x} but found the id {:#04x} instead",
|
|
addr,
|
|
device,
|
|
found_device
|
|
)
|
|
}
|
|
}
|
|
|
|
if let Some(vendor) = vendor {
|
|
if vendor != found_vendor {
|
|
anyhow::bail!(
|
|
"VFIO expects a PCI device on address {} with the vendor id {:#04x} but found the id {:#04x} instead",
|
|
addr,
|
|
vendor,
|
|
found_vendor
|
|
)
|
|
}
|
|
}
|
|
|
|
addr
|
|
}
|
|
|
|
(None, Some(vendor), Some(device)) => {
|
|
let mut counter = index;
|
|
let mut items: Vec<(PCIAddress, u32, u32)> = vec![];
|
|
|
|
for entry in std::fs::read_dir("/sys/bus/pci/devices")? {
|
|
let entry = entry?;
|
|
let file_name = entry.file_name();
|
|
let addr_name = file_name
|
|
.to_str()
|
|
.ok_or_else(|| anyhow::anyhow!("Failed to parse PCI device name"))?;
|
|
let addr = PCIAddress::from_str(addr_name)?;
|
|
let (found_vendor, found_device) = read_pci_ids(&addr)?;
|
|
items.push((addr, found_vendor, found_device));
|
|
}
|
|
|
|
items.sort_by_key(|&(addr, _, _)| addr);
|
|
|
|
for (addr, found_vendor, found_device) in items {
|
|
if found_vendor == vendor && found_device == device {
|
|
if counter == 0 {
|
|
address = Some(addr);
|
|
break;
|
|
}
|
|
|
|
counter -= 1;
|
|
}
|
|
}
|
|
|
|
if let Some(address) = address {
|
|
address
|
|
} else {
|
|
anyhow::bail!(
|
|
"Can't find {}th PCI device with vendor id {:#04x} and device id {:#04x}",
|
|
index + 1,
|
|
vendor,
|
|
device
|
|
)
|
|
}
|
|
}
|
|
|
|
_ => anyhow::bail!("VFIO element needs either vendor and device or address to be set"),
|
|
};
|
|
|
|
let mut cfg = VfioConfig {
|
|
address,
|
|
vendor: None,
|
|
device: None,
|
|
index: 0,
|
|
graphics: false,
|
|
multifunction: false,
|
|
};
|
|
|
|
if let Some(graphics) = table.get("graphics").cloned() {
|
|
cfg.graphics = graphics.into_bool()?;
|
|
}
|
|
|
|
if let Some(multifunction) = table.get("multifunction").cloned() {
|
|
cfg.multifunction = multifunction.into_bool()?;
|
|
}
|
|
|
|
Ok(cfg)
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
|
pub struct SpiceConfig {
|
|
pub enabled: bool,
|
|
pub socket_path: String,
|
|
}
|
|
|
|
impl SpiceConfig {
|
|
pub fn from_table(table: HashMap<String, Value>) -> Result<SpiceConfig, anyhow::Error> {
|
|
let mut cfg = SpiceConfig {
|
|
enabled: false,
|
|
socket_path: "/tmp/win10.sock".to_string(),
|
|
};
|
|
|
|
if let Some(enabled) = table.get("enabled").cloned() {
|
|
cfg.enabled = enabled.into_bool()?;
|
|
}
|
|
|
|
Ok(cfg)
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
|
|
pub struct PCIAddress {
|
|
domain: u32,
|
|
bus: u8,
|
|
slot: u8,
|
|
func: u8,
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for PCIAddress {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
struct X;
|
|
impl Visitor<'_> for X {
|
|
type Value = String;
|
|
|
|
fn expecting(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
|
|
formatter.write_str("Expecting a string")
|
|
}
|
|
|
|
fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
|
|
Ok(v)
|
|
}
|
|
}
|
|
|
|
let x = deserializer.deserialize_string(X)?;
|
|
Ok(PCIAddress::from_str(&x).map_err(|x| de::Error::custom(x))?)
|
|
}
|
|
}
|
|
|
|
impl Serialize for PCIAddress {
|
|
fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
serializer.serialize_str(&self.to_string())
|
|
}
|
|
}
|
|
|
|
impl PCIAddress {
|
|
fn to_string(&self) -> String {
|
|
format!(
|
|
"{:04x}:{:02x}:{:02x}.{:x}",
|
|
self.domain, self.bus, self.slot, self.func
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Debug for PCIAddress {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str("PCIAddress(")?;
|
|
if f.alternate() && self.domain == 0 {
|
|
f.write_str(&format!("{:04x}:", self.domain))?;
|
|
}
|
|
|
|
f.write_str(&format!(
|
|
"{:02x}:{:02x}.{:x}",
|
|
self.bus, self.slot, self.func
|
|
))?;
|
|
f.write_str(")")
|
|
}
|
|
}
|
|
|
|
impl Display for PCIAddress {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
if f.alternate() && self.domain == 0 {
|
|
f.write_str(&format!("{:04x}:", self.domain))?;
|
|
}
|
|
|
|
f.write_str(&format!(
|
|
"{:02x}:{:02x}.{:x}",
|
|
self.bus, self.slot, self.func
|
|
))
|
|
}
|
|
}
|
|
|
|
impl FromStr for PCIAddress {
|
|
type Err = anyhow::Error;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
let mut rev = s.rsplit(":");
|
|
let mut addr = PCIAddress::default();
|
|
|
|
if let Some(slot_and_func) = rev.next() {
|
|
let mut splitter = slot_and_func.split(".");
|
|
|
|
if let Some(slot) = splitter.next() {
|
|
addr.slot = u8::from_str_radix(slot, 16)?;
|
|
}
|
|
|
|
if let Some(func) = splitter.next() {
|
|
addr.func = u8::from_str_radix(func, 16)?;
|
|
}
|
|
}
|
|
|
|
if let Some(bus) = rev.next() {
|
|
addr.bus = u8::from_str_radix(bus, 16)?;
|
|
}
|
|
|
|
if let Some(domain) = rev.next() {
|
|
addr.domain = u32::from_str_radix(domain, 16)?;
|
|
}
|
|
|
|
Ok(addr)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::PCIAddress;
|
|
use std::str::FromStr;
|
|
|
|
#[test]
|
|
fn test_input_and_output_are_same() {
|
|
assert_eq!(
|
|
PCIAddress::from_str("0000:00:00.1")
|
|
.expect("Failed to parse correct string")
|
|
.to_string(),
|
|
"0000:00:00.1"
|
|
);
|
|
|
|
assert_eq!(
|
|
PCIAddress::from_str("0000:00:01.0")
|
|
.expect("Failed to parse correct string")
|
|
.to_string(),
|
|
"0000:00:01.0"
|
|
);
|
|
}
|
|
}
|