blob: 790b2935939b98c8da269918306cf221e72ecbf4 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
fidl_fuchsia_driver_development as fdd,
std::{
collections::{HashMap, HashSet},
rc::Rc,
},
};
/// Represents either a DFv1 device or DFv2 node.
#[derive(Debug, PartialEq)]
pub struct DeviceNode {
pub info: Rc<fdd::NodeInfo>,
pub child_nodes: Vec<DeviceNode>,
/// The number of child devices a device has. This can be different than
/// `child_nodes.len()` if, for example, the call to GetDeviceInfo passes a
/// filter that excludes a device's children from being returned.
pub num_children: usize,
}
impl DeviceNode {
pub fn new(info: Rc<fdd::NodeInfo>, child_nodes: Vec<DeviceNode>, num_children: usize) -> Self {
Self { info, child_nodes, num_children }
}
}
// Used internally for quick lookup of DFv1 devices or DFv2 nodes within a
// hashmap.
struct DeviceVertex {
info: Rc<fdd::NodeInfo>,
parent_ids: HashSet<u64>,
child_ids: HashSet<u64>,
}
impl DeviceVertex {
fn new(info: Rc<fdd::NodeInfo>, parent_ids: HashSet<u64>, child_ids: HashSet<u64>) -> Self {
Self { info, parent_ids, child_ids }
}
}
fn device_vertex_to_device_node(
id: u64,
device_vertices: &HashMap<u64, DeviceVertex>,
) -> DeviceNode {
let vertex = device_vertices.get(&id).expect("Invalid vertex ID");
// If two devices share the same child device then multiple nodes will
// be made for that child.
let child_nodes = vertex
.child_ids
.iter()
.filter_map(|child_id| {
// Child device may or may not exist depending on the filter
// sent in GetDeviceInfo.
if device_vertices.contains_key(child_id) {
Some(device_vertex_to_device_node(*child_id, device_vertices))
} else {
None
}
})
.collect();
DeviceNode::new(Rc::clone(&vertex.info), child_nodes, vertex.child_ids.len())
}
// DFS approach to detect cycles.
// `vertices` acts similar to an adjacency matrix.
fn is_cyclic(vertices: &HashMap<u64, DeviceVertex>) -> bool {
enum State {
Unprocessed,
Processing,
Processed,
}
// Process a vertex and its children for cycles.
// Returns true if there's a cycle and false if there is not.
fn process_vertex(
id: u64,
vertices: &HashMap<u64, DeviceVertex>,
states: &mut HashMap<u64, State>,
) -> bool {
states.insert(id, State::Processing);
if let Some(vertex) = vertices.get(&id) {
for child_id in vertex.child_ids.iter() {
match states.get(child_id) {
// We found another Node that's processing which means
// there's a cycle.
Some(State::Processing) => {
return true;
}
Some(State::Unprocessed) => {
// Check our unprocessed child for cycles and return if
// we find one.
if process_vertex(*child_id, vertices, states) {
return true;
}
}
_ => {}
}
}
}
// We have not found any cycles so mark Node as processed.
states.insert(id, State::Processed);
false
}
// Key: ID of device vertex
let mut states: HashMap<u64, State> =
vertices.keys().map(|id| (*id, State::Unprocessed)).collect();
for id in vertices.keys() {
if let Some(State::Unprocessed) = states.get(id) {
if process_vertex(*id, &vertices, &mut states) {
return true;
}
}
}
false
}
impl From<Rc<fdd::NodeInfo>> for DeviceVertex {
fn from(device_info: Rc<fdd::NodeInfo>) -> Self {
let mut parent_ids = HashSet::<u64>::new();
if let Some(ids) = device_info.parent_ids.as_ref() {
parent_ids = ids.iter().map(|id| *id).collect();
assert_eq!(
ids.len(),
parent_ids.len(),
"Device has the same parent ID listed multiple times"
);
};
let mut child_ids = HashSet::<u64>::new();
if let Some(ids) = device_info.child_ids.as_ref() {
child_ids = ids.iter().map(|id| *id).collect();
assert_eq!(
ids.len(),
child_ids.len(),
"Device has the same child ID listed multiple times"
);
};
Self::new(device_info, parent_ids, child_ids)
}
}
/// Creates zero or more tree structures that reflects the DFv1 devices' or DFv2
/// nodes' topology defined in `device_infos`. Note that if two devices/nodes
/// share a child then that child will be duplicated so that each parent has a
/// copy of the child.
///
/// # Panics
///
/// Panics if one of the resulting graphs is cyclic or if `device_infos`
/// contains conflicting information about the devices'/nodes' topology or if a
/// device's/node's ID or topological path is missing.
pub fn create_device_topology(device_infos: Vec<fdd::NodeInfo>) -> Vec<DeviceNode> {
let device_infos: Vec<Rc<fdd::NodeInfo>> =
device_infos.into_iter().map(|device_info| Rc::new(device_info)).collect();
// Key: ID of the vertex
let device_vertices: HashMap<u64, DeviceVertex> = device_infos
.iter()
.map(|device_info| {
(device_info.id.expect("Device ID missing"), Rc::clone(&device_info).into())
})
.collect();
assert!(!is_cyclic(&device_vertices), "Device topology is cyclic");
// Assert that the devices' children and parents make sense.
let mut root_ids: Vec<u64> = Vec::new();
for device_info in device_infos {
let id = device_info.id.expect("Device ID missing");
// Assert all device's children exist.
if let Some(child_ids) = device_info.child_ids.as_ref() {
for child_id in child_ids {
// Child device may or may not exist depending on the filter
// sent in GetDeviceInfo.
if let Some(child_node) = device_vertices.get(child_id) {
assert!(child_node.parent_ids.contains(&id), "Parent device lists the ID of a child device in it's child ID's, however, the child device does not list the parent device's ID in its parent devices");
}
}
}
// Find root and assert non-root devices' parents exist.
if let Some(parent_ids) = device_info.parent_ids.as_ref() {
for parent_id in parent_ids {
// Parent device may or may not exist depending on the filter
// sent in GetDeviceInfo or if the current device is a root
// device.
if let Some(parent_node) = device_vertices.get(parent_id) {
assert!(parent_node.child_ids.contains(&id), "Child device lists the ID of a parent device in it's parent ID's, however, the parent device does not list the child device's ID in its child devices");
} else {
root_ids.push(id);
}
}
} else {
root_ids.push(id);
}
}
root_ids
.into_iter()
.map(|root_id| device_vertex_to_device_node(root_id, &device_vertices))
.collect::<Vec<DeviceNode>>()
}