From 109970293e1f198020198d28d2eab024c17ffc4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Fri, 21 Nov 2025 16:24:47 +0000 Subject: [PATCH 1/2] Add support for reading nodes in FDT --- src/fdt/mod.rs | 204 ++++++++++++++++++++++++++++++++++- src/fdt/node.rs | 175 ++++++++++++++++++++++++++++++ tests/dtb/test_children.dtb | Bin 0 -> 152 bytes tests/dtb/test_traversal.dtb | Bin 0 -> 141 bytes tests/dts/test_children.dts | 11 ++ tests/dts/test_traversal.dts | 12 +++ tests/fdt.rs | 65 +++++++++++ 7 files changed, 465 insertions(+), 2 deletions(-) create mode 100644 src/fdt/node.rs create mode 100644 tests/dtb/test_children.dtb create mode 100644 tests/dtb/test_traversal.dtb create mode 100644 tests/dts/test_children.dts create mode 100644 tests/dts/test_traversal.dts create mode 100644 tests/fdt.rs diff --git a/src/fdt/mod.rs b/src/fdt/mod.rs index 7404c8a..23d0b0c 100644 --- a/src/fdt/mod.rs +++ b/src/fdt/mod.rs @@ -15,17 +15,25 @@ //! //! [Flattened Device Tree (FDT)]: https://devicetree-specification.readthedocs.io/en/latest/chapter5-flattened-format.html +use crate::error::{FdtError, FdtErrorKind}; +mod node; +use core::ffi::CStr; use core::mem::offset_of; use core::ptr; +pub use node::FdtNode; use zerocopy::byteorder::big_endian; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned}; -use crate::error::{FdtError, FdtErrorKind}; - /// Version of the FDT specification supported by this library. const FDT_VERSION: u32 = 17; +pub(crate) const FDT_TAGSIZE: usize = size_of::(); pub(crate) const FDT_MAGIC: u32 = 0xd00d_feed; +pub(crate) const FDT_BEGIN_NODE: u32 = 0x1; +pub(crate) const FDT_END_NODE: u32 = 0x2; +pub(crate) const FDT_END: u32 = 0x9; +pub(crate) const FDT_PROP: u32 = 0x3; +pub(crate) const FDT_NOP: u32 = 0x4; #[repr(C, packed)] #[derive(Debug, Copy, Clone, FromBytes, IntoBytes, Unaligned, Immutable, KnownLayout)] @@ -100,6 +108,31 @@ pub struct Fdt<'a> { pub(crate) data: &'a [u8], } +/// A token in the device tree structure. +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum FdtToken { + BeginNode, + EndNode, + Prop, + Nop, + End, +} + +impl TryFrom for FdtToken { + type Error = u32; + + fn try_from(value: u32) -> Result { + match value { + FDT_BEGIN_NODE => Ok(FdtToken::BeginNode), + FDT_END_NODE => Ok(FdtToken::EndNode), + FDT_PROP => Ok(FdtToken::Prop), + FDT_NOP => Ok(FdtToken::Nop), + FDT_END => Ok(FdtToken::End), + _ => Err(value), + } + } +} + impl<'a> Fdt<'a> { /// Creates a new `Fdt` from the given byte slice. /// @@ -276,6 +309,173 @@ impl<'a> Fdt<'a> { pub fn boot_cpuid_phys(&self) -> u32 { self.header().boot_cpuid_phys() } + + /// Returns the root node of the device tree. + /// + /// # Errors + /// + /// Returns an [`FdtErrorKind::InvalidLength`] if the FDT structure is + /// truncated or an [`FdtErrorKind::BadToken`] if the first token is not + /// `FDT_BEGIN_NODE`. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::fdt::Fdt; + /// # let dtb = include_bytes!("../../tests/dtb/test.dtb"); + /// let fdt = Fdt::new(dtb).unwrap(); + /// let root = fdt.root().unwrap(); + /// assert_eq!(root.name().unwrap(), ""); + /// ``` + pub fn root(&self) -> Result, FdtError> { + let offset = self.header().off_dt_struct() as usize; + let token = self.read_token(offset)?; + if token != FdtToken::BeginNode { + return Err(FdtError::new( + FdtErrorKind::BadToken(FDT_BEGIN_NODE), + offset, + )); + } + Ok(FdtNode { fdt: self, offset }) + } + + /// Finds a node by its path. + /// + /// # Performance + /// + /// This method traverses the device tree and its performance is linear in + /// 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 + /// + /// ``` + /// # use dtoolkit::fdt::Fdt; + /// # let dtb = include_bytes!("../../tests/dtb/test_traversal.dtb"); + /// let fdt = Fdt::new(dtb).unwrap(); + /// let node = fdt.find_node("/a/b/c").unwrap().unwrap(); + /// assert_eq!(node.name().unwrap(), "c"); + /// ``` + #[must_use] + pub fn find_node(&self, path: &str) -> Option, FdtError>> { + if !path.starts_with('/') { + return None; + } + let mut current_node = match self.root() { + Ok(node) => node, + Err(e) => return Some(Err(e)), + }; + if path == "/" { + return Some(Ok(current_node)); + } + for component in path.split('/').filter(|s| !s.is_empty()) { + match current_node.children().find(|child| { + child + .as_ref() + .is_ok_and(|c| c.name().is_ok_and(|n| n == component)) + }) { + Some(Ok(node)) => current_node = node, + Some(Err(e)) => return Some(Err(e)), + None => return None, + } + } + Some(Ok(current_node)) + } + + pub(crate) fn read_token(&self, offset: usize) -> Result { + let val = big_endian::U32::ref_from_prefix(&self.data[offset..]) + .map(|(val, _)| val.get()) + .map_err(|_e| FdtError::new(FdtErrorKind::InvalidLength, offset))?; + FdtToken::try_from(val).map_err(|t| FdtError::new(FdtErrorKind::BadToken(t), offset)) + } + + /// Return a NUL-terminated string from a given offset. + pub(crate) fn string_at_offset( + &self, + offset: usize, + end: Option, + ) -> Result<&'a str, FdtError> { + let slice = match end { + Some(end) => self.data.get(offset..end), + None => self.data.get(offset..), + }; + let slice = slice.ok_or(FdtError::new(FdtErrorKind::InvalidOffset, offset))?; + + match CStr::from_bytes_until_nul(slice).map(|val| val.to_str()) { + Ok(Ok(val)) => Ok(val), + _ => Err(FdtError::new(FdtErrorKind::InvalidString, offset)), + } + } + + pub(crate) fn find_string_end(&self, start: usize) -> Result { + let mut offset = start; + loop { + match self.data.get(offset) { + Some(0) => return Ok(offset + 1), + Some(_) => {} + None => return Err(FdtError::new(FdtErrorKind::InvalidString, start)), + } + offset += 1; + } + } + + pub(crate) fn next_sibling_offset(&self, mut offset: usize) -> Result { + offset += FDT_TAGSIZE; // Skip FDT_BEGIN_NODE + + // Skip node name + offset = self.find_string_end(offset)?; + offset = Self::align_tag_offset(offset); + + // Skip properties + loop { + let token = self.read_token(offset)?; + match token { + FdtToken::Prop => { + offset += FDT_TAGSIZE; // skip FDT_PROP + offset = self.next_property_offset(offset)?; + } + FdtToken::Nop => offset += FDT_TAGSIZE, + _ => break, + } + } + + // Skip child nodes + loop { + let token = self.read_token(offset)?; + match token { + FdtToken::BeginNode => { + offset = self.next_sibling_offset(offset)?; + } + FdtToken::EndNode => { + offset += FDT_TAGSIZE; + break; + } + FdtToken::Nop => offset += FDT_TAGSIZE, + _ => {} + } + } + + Ok(offset) + } + + pub(crate) fn next_property_offset(&self, mut offset: usize) -> Result { + let len = big_endian::U32::ref_from_prefix(&self.data[offset..]) + .map(|(val, _)| val.get()) + .map_err(|_e| FdtError::new(FdtErrorKind::InvalidLength, offset))? + as usize; + offset += FDT_TAGSIZE; // skip value length + offset += FDT_TAGSIZE; // skip name offset + offset += len; // skip property value + + Ok(Self::align_tag_offset(offset)) + } + + pub(crate) fn align_tag_offset(offset: usize) -> usize { + offset.next_multiple_of(FDT_TAGSIZE) + } } #[cfg(test)] diff --git a/src/fdt/node.rs b/src/fdt/node.rs new file mode 100644 index 0000000..d8276b3 --- /dev/null +++ b/src/fdt/node.rs @@ -0,0 +1,175 @@ +// 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-only API for inspecting a device tree node. + +use super::{FDT_TAGSIZE, Fdt, FdtToken}; +use crate::error::FdtError; + +/// A node in a flattened device tree. +#[derive(Debug, Clone, Copy)] +pub struct FdtNode<'a> { + pub(crate) fdt: &'a Fdt<'a>, + pub(crate) offset: usize, +} + +impl<'a> FdtNode<'a> { + /// Returns the name of this node. + /// + /// # Examples + /// + /// # Errors + /// + /// Returns an + /// [`FdtErrorKind::InvalidOffset`](crate::error::FdtErrorKind::InvalidOffset) + /// if the name offset is invalid or an + /// [`FdtErrorKind::InvalidString`](crate::error::FdtErrorKind::InvalidString) if the string at the offset is not null-terminated + /// or contains invalid UTF-8. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::fdt::Fdt; + /// # let dtb = include_bytes!("../../tests/dtb/test_children.dtb"); + /// let fdt = Fdt::new(dtb).unwrap(); + /// let root = fdt.root().unwrap(); + /// let child = root.child("child1").unwrap().unwrap(); + /// assert_eq!(child.name().unwrap(), "child1"); + /// ``` + pub fn name(&self) -> Result<&'a str, FdtError> { + let name_offset = self.offset + FDT_TAGSIZE; + self.fdt.string_at_offset(name_offset, None) + } + + /// Returns a child node by its name. + /// + /// # Performance + /// + /// This method's performance is linear in the number of children of this + /// 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 + /// + /// Returns an error if a child node's name cannot be read. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::fdt::Fdt; + /// # let dtb = include_bytes!("../../tests/dtb/test_children.dtb"); + /// let fdt = Fdt::new(dtb).unwrap(); + /// let root = fdt.root().unwrap(); + /// let child = root.child("child1").unwrap().unwrap(); + /// assert_eq!(child.name().unwrap(), "child1"); + /// ``` + pub fn child(&self, name: &str) -> Result>, FdtError> { + for child in self.children() { + let child = child?; + if child.name()? == name { + return Ok(Some(child)); + } + } + Ok(None) + } + + /// Returns an iterator over the children of this node. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::fdt::Fdt; + /// # let dtb = include_bytes!("../../tests/dtb/test_children.dtb"); + /// let fdt = Fdt::new(dtb).unwrap(); + /// let root = fdt.root().unwrap(); + /// let mut children = root.children(); + /// assert_eq!(children.next().unwrap().unwrap().name().unwrap(), "child1"); + /// assert_eq!(children.next().unwrap().unwrap().name().unwrap(), "child2"); + /// assert!(children.next().is_none()); + /// ``` + pub fn children(&self) -> impl Iterator, FdtError>> + use<'a> { + FdtChildIter::Start { + fdt: self.fdt, + offset: self.offset, + } + } +} + +/// An iterator over the children of a device tree node. +enum FdtChildIter<'a> { + Start { fdt: &'a Fdt<'a>, offset: usize }, + Running { fdt: &'a Fdt<'a>, offset: usize }, + Error, +} + +impl<'a> Iterator for FdtChildIter<'a> { + type Item = Result, FdtError>; + + fn next(&mut self) -> Option { + match self { + Self::Start { fdt, offset } => { + let mut offset = *offset; + offset += FDT_TAGSIZE; // Skip FDT_BEGIN_NODE + offset = match fdt.find_string_end(offset) { + Ok(offset) => offset, + Err(e) => { + *self = Self::Error; + return Some(Err(e)); + } + }; + offset = Fdt::align_tag_offset(offset); + *self = Self::Running { fdt, offset }; + self.next() + } + Self::Running { fdt, offset } => match Self::try_next(fdt, offset) { + Some(Ok(val)) => Some(Ok(val)), + Some(Err(e)) => { + *self = Self::Error; + Some(Err(e)) + } + None => None, + }, + Self::Error => None, + } + } +} + +impl<'a> FdtChildIter<'a> { + fn try_next(fdt: &'a Fdt<'a>, offset: &mut usize) -> Option, FdtError>> { + loop { + let token = match fdt.read_token(*offset) { + Ok(token) => token, + Err(e) => return Some(Err(e)), + }; + match token { + FdtToken::BeginNode => { + let node_offset = *offset; + *offset = match fdt.next_sibling_offset(*offset) { + Ok(offset) => offset, + Err(e) => return Some(Err(e)), + }; + return Some(Ok(FdtNode { + fdt, + offset: node_offset, + })); + } + FdtToken::Prop => { + *offset = match fdt.next_property_offset(*offset + FDT_TAGSIZE) { + Ok(offset) => offset, + Err(e) => return Some(Err(e)), + }; + } + FdtToken::EndNode | FdtToken::End => return None, + FdtToken::Nop => *offset += FDT_TAGSIZE, + } + } + } +} diff --git a/tests/dtb/test_children.dtb b/tests/dtb/test_children.dtb new file mode 100644 index 0000000000000000000000000000000000000000..e2f317362bec65ba70914fbe9d87d65099138324 GIT binary patch literal 152 zcmcb>`|m9S1H%j;wgBQDAl3k4K_C_YVi4c~;t((km0*N&8Iv`|m9S14AzmTL5tf5NiOjAP@@xF$k~%aR8WwN-#pXjEN8iV-l23hS1DFiUq=C Wxdfz{kT7EkL;@zpSx}T;zyJX4b_nzU literal 0 HcmV?d00001 diff --git a/tests/dts/test_children.dts b/tests/dts/test_children.dts new file mode 100644 index 0000000..37e0612 --- /dev/null +++ b/tests/dts/test_children.dts @@ -0,0 +1,11 @@ +/dts-v1/; + +/ { + child1 { + prop1 = <0x12345678>; + }; + + child2 { + prop2 = "hello"; + }; +}; diff --git a/tests/dts/test_traversal.dts b/tests/dts/test_traversal.dts new file mode 100644 index 0000000..40ddd00 --- /dev/null +++ b/tests/dts/test_traversal.dts @@ -0,0 +1,12 @@ +/dts-v1/; + +/ { + a { + b { + c { + prop = <1234>; + }; + }; + }; + d {}; +}; diff --git a/tests/fdt.rs b/tests/fdt.rs new file mode 100644 index 0000000..4a7cc4d --- /dev/null +++ b/tests/fdt.rs @@ -0,0 +1,65 @@ +// 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 dtoolkit::fdt::Fdt; + +#[test] +fn read_child_nodes() { + let dtb = include_bytes!("dtb/test_children.dtb"); + let fdt = Fdt::new(dtb).unwrap(); + let root = fdt.root().unwrap(); + let mut children = root.children(); + + let child1 = children.next().unwrap().unwrap(); + assert_eq!(child1.name().unwrap(), "child1"); + + let child2 = children.next().unwrap().unwrap(); + assert_eq!(child2.name().unwrap(), "child2"); + + assert!(children.next().is_none()); +} + +#[test] +fn get_child_by_name() { + let dtb = include_bytes!("dtb/test_children.dtb"); + let fdt = Fdt::new(dtb).unwrap(); + let root = fdt.root().unwrap(); + + let child1 = root.child("child1").unwrap().unwrap(); + assert_eq!(child1.name().unwrap(), "child1"); + + let child2 = root.child("child2").unwrap().unwrap(); + assert_eq!(child2.name().unwrap(), "child2"); + + assert!(root.child("non-existent-child").unwrap().is_none()); +} + +#[test] +fn find_node_by_path() { + let dtb = include_bytes!("dtb/test_traversal.dtb"); + let fdt = Fdt::new(dtb).unwrap(); + + let root = fdt.find_node("/").unwrap().unwrap(); + assert_eq!(root.name().unwrap(), ""); + + let a = fdt.find_node("/a").unwrap().unwrap(); + assert_eq!(a.name().unwrap(), "a"); + + let b = fdt.find_node("/a/b").unwrap().unwrap(); + assert_eq!(b.name().unwrap(), "b"); + + let c = fdt.find_node("/a/b/c").unwrap().unwrap(); + assert_eq!(c.name().unwrap(), "c"); + + let d = fdt.find_node("/d").unwrap().unwrap(); + assert_eq!(d.name().unwrap(), "d"); + + assert!(fdt.find_node("/a/c").is_none()); + assert!(fdt.find_node("/x").is_none()); + assert!(fdt.find_node("").is_none()); +} From 9b7da6c7684df3da8bc812cdb73c09f24d0f94f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Mon, 8 Dec 2025 15:35:05 +0000 Subject: [PATCH 2/2] address review comments --- src/fdt/mod.rs | 8 ++------ src/fdt/node.rs | 6 +----- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/fdt/mod.rs b/src/fdt/mod.rs index 23d0b0c..ff5e5ee 100644 --- a/src/fdt/mod.rs +++ b/src/fdt/mod.rs @@ -109,7 +109,7 @@ pub struct Fdt<'a> { } /// A token in the device tree structure. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum FdtToken { BeginNode, EndNode, @@ -344,11 +344,7 @@ impl<'a> Fdt<'a> { /// # Performance /// /// This method traverses the device tree and its performance is linear in - /// 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. + /// the number of nodes in the path. /// /// # Examples /// diff --git a/src/fdt/node.rs b/src/fdt/node.rs index d8276b3..77b4aed 100644 --- a/src/fdt/node.rs +++ b/src/fdt/node.rs @@ -51,11 +51,7 @@ 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. 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. + /// node because it iterates through the children. /// /// # Errors ///