diff --git a/Cargo.toml b/Cargo.toml index 7c37c21..8770b92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,11 @@ keywords = ["device-tree", "fdt", "dtb", "dts", "parser", "no_std"] [features] default = [] -alloc = [] -std = ["alloc"] +write = ["dep:indexmap", "dep:twox-hash"] [dependencies] +indexmap = { version = "2", optional = true, default-features = false } +twox-hash = { version = "2", optional = true, features = ["xxhash64"], default-features = false } zerocopy = { version = "0.8.28", features = ["derive"] } [lints.rust] diff --git a/src/fdt/mod.rs b/src/fdt/mod.rs index 00e1893..acd05d8 100644 --- a/src/fdt/mod.rs +++ b/src/fdt/mod.rs @@ -375,7 +375,11 @@ impl<'a> Fdt<'a> { /// # Performance /// /// This method traverses the device tree and its performance is linear in - /// the number of nodes in the path. + /// the number of nodes in the path. If you need to call this often, + /// consider using + /// [`DeviceTree::from_fdt`](crate::model::DeviceTree::from_fdt) + /// first. [`DeviceTree`](crate::model::DeviceTree) stores the nodes in a + /// hash map for constant-time lookup. /// /// # Examples /// diff --git a/src/fdt/node.rs b/src/fdt/node.rs index 30d252f..07d6de2 100644 --- a/src/fdt/node.rs +++ b/src/fdt/node.rs @@ -105,7 +105,11 @@ impl<'a> FdtNode<'a> { /// # Performance /// /// This method's performance is linear in the number of children of this - /// node because it iterates through the children. + /// node because it iterates through the children. If you need to call this + /// often, consider converting to a + /// [`DeviceTreeNode`](crate::model::DeviceTreeNode) first. Child lookup + /// on a [`DeviceTreeNode`](crate::model::DeviceTreeNode) is a + /// constant-time operation. /// /// # Errors /// diff --git a/src/lib.rs b/src/lib.rs index 0bf60bd..9fcf740 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,11 @@ #![warn(missing_docs, rustdoc::missing_crate_level_docs)] #![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(feature = "write")] +extern crate alloc; + pub mod error; pub mod fdt; pub mod memreserve; +#[cfg(feature = "write")] +pub mod model; diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..d22c4ec --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,127 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! A read-write, in-memory representation of a device tree. +//! +//! This module provides the [`DeviceTree`], [`DeviceTreeNode`], and +//! [`DeviceTreeProperty`] structs, which can be used to create or modify a +//! device tree in memory. The [`DeviceTree`] can then be serialized to a +//! flattened device tree blob. + +use alloc::vec::Vec; + +use crate::error::FdtError; +use crate::fdt::Fdt; +use crate::memreserve::MemoryReservation; +mod node; +mod property; +pub use node::{DeviceTreeNode, DeviceTreeNodeBuilder}; +pub use property::DeviceTreeProperty; + +/// A mutable, in-memory representation of a device tree. +/// +/// This struct provides a high-level API for creating and modifying a device +/// tree. It can be created from scratch or by parsing an existing FDT blob. +/// +/// # Examples +/// +/// ``` +/// # use dtoolkit::model::{DeviceTree, DeviceTreeNode}; +/// let mut tree = DeviceTree::new(); +/// tree.root.add_child(DeviceTreeNode::new("child")); +/// let child = tree.find_node_mut("/child").unwrap(); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct DeviceTree { + /// The root node for this device tree. + pub root: DeviceTreeNode, + /// The memory reservations for this device tree. + pub memory_reservations: Vec, +} + +impl DeviceTree { + /// Creates a new `DeviceTree` with the given root node. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::{DeviceTree, DeviceTreeNode}; + /// let tree = DeviceTree::new(); + /// ``` + #[must_use] + pub fn new() -> Self { + Self { + root: DeviceTreeNode::new("/"), + memory_reservations: Vec::new(), + } + } + + /// Creates a new `DeviceTree` from a `Fdt`. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::{fdt::Fdt, model::DeviceTree}; + /// # let dtb = include_bytes!("../../tests/dtb/test.dtb"); + /// let fdt = Fdt::new(dtb).unwrap(); + /// let tree = DeviceTree::from_fdt(&fdt).unwrap(); + /// ``` + /// + /// # Errors + /// + /// Returns an error if the root node of the `Fdt` cannot be parsed. + pub fn from_fdt(fdt: &Fdt<'_>) -> Result { + let root = DeviceTreeNode::try_from(fdt.root()?)?; + let memory_reservations: Result, _> = fdt.memory_reservations().collect(); + Ok(DeviceTree { + root, + memory_reservations: memory_reservations?, + }) + } + + /// Finds a node by its path and returns a mutable reference to it. + /// + /// # Performance + /// + /// This method traverses the device tree, but since child lookup is a + /// constant-time operation, performance is linear in the number of path + /// segments. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::{DeviceTree, DeviceTreeNode}; + /// let mut tree = DeviceTree::new(); + /// tree.root.add_child(DeviceTreeNode::new("child")); + /// let child = tree.find_node_mut("/child").unwrap(); + /// assert_eq!(child.name(), "child"); + /// ``` + pub fn find_node_mut(&mut self, path: &str) -> Option<&mut DeviceTreeNode> { + if !path.starts_with('/') { + return None; + } + let mut current_node = &mut self.root; + if path == "/" { + return Some(current_node); + } + for component in path.split('/').filter(|s| !s.is_empty()) { + match current_node.child_mut(component) { + Some(node) => current_node = node, + None => return None, + } + } + Some(current_node) + } +} + +impl Default for DeviceTree { + fn default() -> Self { + Self::new() + } +} diff --git a/src/model/node.rs b/src/model/node.rs new file mode 100644 index 0000000..af5824e --- /dev/null +++ b/src/model/node.rs @@ -0,0 +1,329 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use alloc::borrow::ToOwned; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; + +use indexmap::IndexMap; +use twox_hash::xxhash64; + +use super::property::DeviceTreeProperty; +use crate::error::FdtError; +use crate::fdt::FdtNode; + +/// A mutable, in-memory representation of a device tree node. +/// +/// Children and properties are stored in [`IndexMap`]s, which provide O(1) +/// lookups by name while preserving insertion order. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceTreeNode { + name: String, + pub(super) properties: IndexMap, + pub(super) children: IndexMap, +} + +impl Default for DeviceTreeNode { + fn default() -> Self { + Self { + name: String::new(), + properties: IndexMap::with_hasher(default_hash_state()), + children: IndexMap::with_hasher(default_hash_state()), + } + } +} + +impl DeviceTreeNode { + /// Creates a new [`DeviceTreeNode`] with the given name. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::DeviceTreeNode; + /// let node = DeviceTreeNode::new("my-node"); + /// assert_eq!(node.name(), "my-node"); + /// ``` + #[must_use] + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + ..Default::default() + } + } + + /// Creates a new [`DeviceTreeNodeBuilder`] with the given name. + #[must_use] + pub fn builder(name: impl Into) -> DeviceTreeNodeBuilder { + DeviceTreeNodeBuilder::new(name) + } + + /// Returns the name of this node. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::DeviceTreeNode; + /// let node = DeviceTreeNode::new("my-node"); + /// assert_eq!(node.name(), "my-node"); + /// ``` + #[must_use] + pub fn name(&self) -> &str { + &self.name + } + + /// Returns an iterator over the properties of this node. + pub fn properties(&self) -> impl Iterator { + self.properties.values() + } + + /// Returns a mutable iterator over the properties of this node. + pub fn properties_mut(&mut self) -> impl Iterator { + self.properties.values_mut() + } + + /// Finds a property by its name and returns a reference to it. + /// + /// # Performance + /// + /// This is a constant-time operation. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::{DeviceTreeNode, DeviceTreeProperty}; + /// let mut node = DeviceTreeNode::new("my-node"); + /// node.add_property(DeviceTreeProperty::new("my-prop", vec![1, 2, 3, 4])); + /// let prop = node.property("my-prop").unwrap(); + /// assert_eq!(prop.value(), &[1, 2, 3, 4]); + /// ``` + #[must_use] + pub fn property(&self, name: &str) -> Option<&DeviceTreeProperty> { + self.properties.get(name) + } + + /// Finds a property by its name and returns a mutable reference to it. + /// + /// # Performance + /// + /// This is a constant-time operation. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::{DeviceTreeNode, DeviceTreeProperty}; + /// let mut node = DeviceTreeNode::new("my-node"); + /// node.add_property(DeviceTreeProperty::new("my-prop", vec![1, 2, 3, 4])); + /// let prop = node.property_mut("my-prop").unwrap(); + /// prop.set_value(vec![5, 6, 7, 8]); + /// assert_eq!(prop.value(), &[5, 6, 7, 8]); + /// ``` + #[must_use] + pub fn property_mut(&mut self, name: &str) -> Option<&mut DeviceTreeProperty> { + self.properties.get_mut(name) + } + + /// Adds a property to this node. + /// + /// # Performance + /// + /// This is a constant-time operation. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::{DeviceTreeNode, DeviceTreeProperty}; + /// let mut node = DeviceTreeNode::new("my-node"); + /// node.add_property(DeviceTreeProperty::new("my-prop", vec![1, 2, 3, 4])); + /// assert_eq!(node.property("my-prop").unwrap().value(), &[1, 2, 3, 4]); + /// ``` + pub fn add_property(&mut self, property: DeviceTreeProperty) { + self.properties.insert(property.name().to_owned(), property); + } + + /// Removes a property from this node by its name. + /// + /// # Performance + /// + /// This is a linear-time operation, as it needs to shift elements after + /// the removed property. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::{DeviceTreeNode, DeviceTreeProperty}; + /// let mut node = DeviceTreeNode::new("my-node"); + /// node.add_property(DeviceTreeProperty::new("my-prop", vec![1, 2, 3, 4])); + /// let prop = node.remove_property("my-prop").unwrap(); + /// assert_eq!(prop.value(), &[1, 2, 3, 4]); + /// assert!(node.property("my-prop").is_none()); + /// ``` + pub fn remove_property(&mut self, name: &str) -> Option { + self.properties.shift_remove(name) + } + + /// Returns an iterator over the children of this node. + pub fn children(&self) -> impl Iterator { + self.children.values() + } + + /// Returns a mutable iterator over the children of this node. + pub fn children_mut(&mut self) -> impl Iterator { + self.children.values_mut() + } + + /// Finds a child by its name and returns a reference to it. + /// + /// # Performance + /// + /// This is a constant-time operation. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::{DeviceTreeNode, DeviceTreeProperty}; + /// let mut node = DeviceTreeNode::new("my-node"); + /// node.add_child(DeviceTreeNode::new("child")); + /// let child = node.child("child"); + /// assert!(child.is_some()); + /// ``` + #[must_use] + pub fn child(&self, name: &str) -> Option<&DeviceTreeNode> { + self.children.get(name) + } + + /// Finds a child by its name and returns a mutable reference to it. + /// + /// # Performance + /// + /// This is a constant-time operation. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::{DeviceTreeNode, DeviceTreeProperty}; + /// let mut node = DeviceTreeNode::new("my-node"); + /// node.add_child(DeviceTreeNode::new("child")); + /// let child = node.child_mut("child").unwrap(); + /// child.add_property(DeviceTreeProperty::new("my-prop", vec![1, 2, 3, 4])); + /// assert_eq!(child.property("my-prop").unwrap().value(), &[1, 2, 3, 4]); + /// ``` + #[must_use] + pub fn child_mut(&mut self, name: &str) -> Option<&mut DeviceTreeNode> { + self.children.get_mut(name) + } + + /// Adds a child to this node. + /// + /// # Performance + /// + /// This is a constant-time operation. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::DeviceTreeNode; + /// let mut node = DeviceTreeNode::new("my-node"); + /// node.add_child(DeviceTreeNode::new("child")); + /// assert_eq!(node.child("child").unwrap().name(), "child"); + /// ``` + pub fn add_child(&mut self, child: DeviceTreeNode) { + self.children.insert(child.name().to_owned(), child); + } + + /// Removes a child from this node by its name. + /// + /// # Performance + /// + /// This is a linear-time operation, as it needs to shift elements after + /// the removed child. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::DeviceTreeNode; + /// let mut node = DeviceTreeNode::new("my-node"); + /// node.add_child(DeviceTreeNode::new("child")); + /// let child = node.remove_child("child").unwrap(); + /// assert_eq!(child.name(), "child"); + /// assert!(node.child("child").is_none()); + /// ``` + pub fn remove_child(&mut self, name: &str) -> Option { + self.children.shift_remove(name) + } +} + +impl<'a> TryFrom> for DeviceTreeNode { + type Error = FdtError; + + fn try_from(node: FdtNode<'a>) -> Result { + let name = node.name()?.to_string(); + let properties = node + .properties() + .map(|property| property?.try_into()) + .collect::, _>>()?; + let mut property_map = + IndexMap::with_capacity_and_hasher(properties.len(), default_hash_state()); + for property in properties { + property_map.insert(property.name().to_owned(), property); + } + + let children_vec: Vec = node + .children() + .map(|child| child?.try_into()) + .collect::, _>>()?; + let mut children = + IndexMap::with_capacity_and_hasher(children_vec.len(), default_hash_state()); + for child in children_vec { + children.insert(child.name().to_owned(), child); + } + + Ok(DeviceTreeNode { + name, + properties: property_map, + children, + }) + } +} + +/// A builder for creating [`DeviceTreeNode`]s. +#[derive(Debug, Default)] +pub struct DeviceTreeNodeBuilder { + node: DeviceTreeNode, +} + +impl DeviceTreeNodeBuilder { + fn new(name: impl Into) -> Self { + Self { + node: DeviceTreeNode::new(name), + } + } + + /// Adds a property to the node. + #[must_use] + pub fn property(mut self, property: DeviceTreeProperty) -> Self { + self.node.add_property(property); + self + } + + /// Adds a child to the node. + #[must_use] + pub fn child(mut self, child: DeviceTreeNode) -> Self { + self.node.add_child(child); + self + } + + /// Builds the `DeviceTreeNode`. + #[must_use] + pub fn build(self) -> DeviceTreeNode { + self.node + } +} + +fn default_hash_state() -> xxhash64::State { + xxhash64::State::with_seed(0xC001_C0DE) +} diff --git a/src/model/property.rs b/src/model/property.rs new file mode 100644 index 0000000..93dc1cb --- /dev/null +++ b/src/model/property.rs @@ -0,0 +1,136 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::{fmt, str}; + +use crate::error::FdtError; +use crate::fdt::FdtProperty; + +/// An error that can occur when parsing a property. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PropertyError { + /// The property's value has an invalid length for the requested conversion. + InvalidLength, + /// The property's value is not a valid string. + InvalidString, +} + +impl fmt::Display for PropertyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PropertyError::InvalidLength => write!(f, "property has an invalid length"), + PropertyError::InvalidString => write!(f, "property is not a valid string"), + } + } +} + +impl core::error::Error for PropertyError {} + +/// A mutable, in-memory representation of a device tree property. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceTreeProperty { + name: String, + value: Vec, +} + +impl DeviceTreeProperty { + /// Creates a new `DeviceTreeProperty` with the given name and value. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::DeviceTreeProperty; + /// let prop = DeviceTreeProperty::new("my-prop", vec![1, 2, 3, 4]); + /// assert_eq!(prop.name(), "my-prop"); + /// assert_eq!(prop.value(), &[1, 2, 3, 4]); + /// ``` + #[must_use] + pub fn new(name: impl Into, value: impl Into>) -> Self { + Self { + name: name.into(), + value: value.into(), + } + } + + /// Returns the name of this property. + #[must_use] + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the value of this property. + #[must_use] + pub fn value(&self) -> &[u8] { + &self.value + } + + /// Sets the value of this property. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::DeviceTreeProperty; + /// let mut prop = DeviceTreeProperty::new("my-prop", vec![1, 2, 3, 4]); + /// prop.set_value(vec![5, 6, 7, 8]); + /// assert_eq!(prop.value(), &[5, 6, 7, 8]); + /// ``` + pub fn set_value(&mut self, value: impl Into>) { + self.value = value.into(); + } + + /// Returns the value of this property as a `u32`. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::DeviceTreeProperty; + /// let prop = DeviceTreeProperty::new("my-prop", 1234u32.to_be_bytes()); + /// assert_eq!(prop.as_u32(), Ok(1234)); + /// ``` + /// + /// # Errors + /// + /// Returns an error if the property's value is not 4 bytes long. + pub fn as_u32(&self) -> Result { + self.value + .as_slice() + .try_into() + .map(u32::from_be_bytes) + .map_err(|_| PropertyError::InvalidLength) + } + + /// Returns the value of this property as a string. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::model::DeviceTreeProperty; + /// let prop = DeviceTreeProperty::new("my-prop", "hello"); + /// assert_eq!(prop.as_str(), Ok("hello")); + /// ``` + /// # Errors + /// + /// Returns an error if the property's value is not a valid UTF-8 string. + pub fn as_str(&self) -> Result<&str, PropertyError> { + str::from_utf8(&self.value) + .map(|s| s.trim_end_matches('\0')) + .map_err(|_| PropertyError::InvalidString) + } +} + +impl<'a> TryFrom> for DeviceTreeProperty { + type Error = FdtError; + + fn try_from(prop: FdtProperty<'a>) -> Result { + let name = prop.name().to_string(); + let value = prop.value().to_vec(); + Ok(DeviceTreeProperty { name, value }) + } +} diff --git a/tests/model.rs b/tests/model.rs new file mode 100644 index 0000000..ec2065f --- /dev/null +++ b/tests/model.rs @@ -0,0 +1,111 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![cfg(feature = "write")] + +use dtoolkit::model::{DeviceTree, DeviceTreeNode, DeviceTreeProperty}; + +#[test] +fn tree_creation() { + let mut tree = DeviceTree::new(); + tree.root + .add_property(DeviceTreeProperty::new("compatible", "test")); + tree.root + .add_property(DeviceTreeProperty::new("prop-u32", 1u32.to_be_bytes())); + tree.root.add_child( + DeviceTreeNode::builder("child-a") + .property(DeviceTreeProperty::new("child-prop", "a")) + .build(), + ); + tree.root.add_child( + DeviceTreeNode::builder("child-b") + .property(DeviceTreeProperty::new("child-prop", "b")) + .build(), + ); + + let root = &tree.root; + assert_eq!(root.name(), "/"); + assert_eq!(root.properties().count(), 2); + assert_eq!(root.children().count(), 2); + + let child_a = root.children().find(|c| c.name() == "child-a").unwrap(); + assert_eq!(child_a.property("child-prop").unwrap().as_str(), Ok("a")); + + let child_b = root.children().find(|c| c.name() == "child-b").unwrap(); + assert_eq!(child_b.property("child-prop").unwrap().as_str(), Ok("b")); +} + +#[test] +fn tree_modification() { + let mut tree = DeviceTree::new(); + + // Add a child + let child = DeviceTreeNode::new("child"); + tree.root.add_child(child); + assert_eq!(tree.root.children().count(), 1); + + // Add a property to the child + let child = tree.root.child_mut("child").unwrap(); + child.add_property(DeviceTreeProperty::new("prop", "value")); + assert_eq!(child.properties().count(), 1); + + // Find and modify the property + let prop = tree + .root + .child_mut("child") + .unwrap() + .property_mut("prop") + .unwrap(); + prop.set_value("new-value".as_bytes()); + + // Verify the modification + let child = tree.root.children().find(|c| c.name() == "child").unwrap(); + assert_eq!(child.property("prop").unwrap().as_str(), Ok("new-value")); + + // Remove the property + let child = tree.root.child_mut("child").unwrap(); + let removed_prop = child.remove_property("prop"); + assert!(removed_prop.is_some()); + assert_eq!(child.properties().count(), 0); + + // Remove the child + let removed_child = tree.root.remove_child("child"); + assert!(removed_child.is_some()); + assert_eq!(tree.root.children().count(), 0); +} + +#[test] +fn find_node_mut() { + let mut tree = DeviceTree::new(); + tree.root.add_child( + DeviceTreeNode::builder("child-a") + .child(DeviceTreeNode::builder("child-a-a").build()) + .build(), + ); + tree.root + .add_child(DeviceTreeNode::builder("child-b").build()); + + // Find a nested child and modify it + let child_a_a = tree.find_node_mut("/child-a/child-a-a").unwrap(); + child_a_a.add_property(DeviceTreeProperty::new("prop", "value")); + + // Verify the modification + let child_a = tree + .root + .children() + .find(|c| c.name() == "child-a") + .unwrap(); + let child_a_a = child_a + .children() + .find(|c| c.name() == "child-a-a") + .unwrap(); + assert_eq!(child_a_a.property("prop").unwrap().as_str(), Ok("value")); + + // Find a non-existent node + assert!(tree.find_node_mut("/child-a/child-c").is_none()); +}