diff --git a/src/fdt/mod.rs b/src/fdt/mod.rs index ff5e5ee..ebe14de 100644 --- a/src/fdt/mod.rs +++ b/src/fdt/mod.rs @@ -17,11 +17,13 @@ use crate::error::{FdtError, FdtErrorKind}; mod node; +mod property; use core::ffi::CStr; use core::mem::offset_of; use core::ptr; pub use node::FdtNode; +pub use property::FdtProperty; use zerocopy::byteorder::big_endian; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned}; @@ -388,7 +390,22 @@ impl<'a> Fdt<'a> { FdtToken::try_from(val).map_err(|t| FdtError::new(FdtErrorKind::BadToken(t), offset)) } - /// Return a NUL-terminated string from a given offset. + /// Returns a string from the string block. + pub(crate) fn string(&self, string_block_offset: usize) -> Result<&'a str, FdtError> { + let header = self.header(); + let str_block_start = header.off_dt_strings() as usize; + let str_block_size = header.size_dt_strings() as usize; + let str_block_end = str_block_start + str_block_size; + let str_start = str_block_start + string_block_offset; + + if str_start >= str_block_end { + return Err(FdtError::new(FdtErrorKind::InvalidLength, str_start)); + } + + self.string_at_offset(str_start, Some(str_block_end)) + } + + /// Returns a NUL-terminated string from a given offset. pub(crate) fn string_at_offset( &self, offset: usize, diff --git a/src/fdt/node.rs b/src/fdt/node.rs index 77b4aed..2e77309 100644 --- a/src/fdt/node.rs +++ b/src/fdt/node.rs @@ -10,6 +10,7 @@ use super::{FDT_TAGSIZE, Fdt, FdtToken}; use crate::error::FdtError; +use crate::fdt::property::{FdtPropIter, FdtProperty}; /// A node in a flattened device tree. #[derive(Debug, Clone, Copy)] @@ -46,6 +47,57 @@ impl<'a> FdtNode<'a> { self.fdt.string_at_offset(name_offset, None) } + /// Returns a property by its name. + /// + /// # Performance + /// + /// This method iterates through all properties of the node. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::fdt::Fdt; + /// # let dtb = include_bytes!("../../tests/dtb/test_props.dtb"); + /// let fdt = Fdt::new(dtb).unwrap(); + /// let node = fdt.find_node("/test-props").unwrap().unwrap(); + /// let prop = node.property("u32-prop").unwrap().unwrap(); + /// assert_eq!(prop.name(), "u32-prop"); + /// ``` + /// + /// # Errors + /// + /// Returns an error if a property's name or value cannot be read. + pub fn property(&self, name: &str) -> Result>, FdtError> { + for property in self.properties() { + let property = property?; + if property.name() == name { + return Ok(Some(property)); + } + } + Ok(None) + } + + /// Returns an iterator over the properties of this node. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::fdt::Fdt; + /// # let dtb = include_bytes!("../../tests/dtb/test_props.dtb"); + /// let fdt = Fdt::new(dtb).unwrap(); + /// let node = fdt.find_node("/test-props").unwrap().unwrap(); + /// let mut props = node.properties(); + /// assert_eq!(props.next().unwrap().unwrap().name(), "u32-prop"); + /// assert_eq!(props.next().unwrap().unwrap().name(), "u64-prop"); + /// assert_eq!(props.next().unwrap().unwrap().name(), "str-prop"); + /// ``` + pub fn properties(&self) -> impl Iterator, FdtError>> + use<'a> { + FdtPropIter::Start { + fdt: self.fdt, + offset: self.offset, + } + } + /// Returns a child node by its name. /// /// # Performance diff --git a/src/fdt/property.rs b/src/fdt/property.rs new file mode 100644 index 0000000..5477e29 --- /dev/null +++ b/src/fdt/property.rs @@ -0,0 +1,230 @@ +// 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 property. + +use core::ffi::CStr; + +use zerocopy::{FromBytes, big_endian}; + +use super::{FDT_TAGSIZE, Fdt, FdtToken}; +use crate::error::{FdtError, FdtErrorKind}; + +/// A property of a device tree node. +#[derive(Debug, PartialEq)] +pub struct FdtProperty<'a> { + name: &'a str, + value: &'a [u8], + value_offset: usize, +} + +impl<'a> FdtProperty<'a> { + /// Returns the name of this property. + #[must_use] + pub fn name(&self) -> &'a str { + self.name + } + + /// Returns the value of this property. + #[must_use] + pub fn value(&self) -> &'a [u8] { + self.value + } + + /// Returns the value of this property as a `u32`. + /// + /// # Errors + /// + /// Returns an [`FdtErrorKind::InvalidLength`] if the property's value is + /// not 4 bytes long. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::fdt::Fdt; + /// # let dtb = include_bytes!("../../tests/dtb/test_props.dtb"); + /// let fdt = Fdt::new(dtb).unwrap(); + /// let node = fdt.find_node("/test-props").unwrap().unwrap(); + /// let prop = node.property("u32-prop").unwrap().unwrap(); + /// assert_eq!(prop.as_u32().unwrap(), 0x12345678); + /// ``` + pub fn as_u32(&self) -> Result { + big_endian::U32::ref_from_bytes(self.value) + .map(|val| val.get()) + .map_err(|_e| FdtError::new(FdtErrorKind::InvalidLength, self.value_offset)) + } + + /// Returns the value of this property as a `u64`. + /// + /// # Errors + /// + /// Returns an [`FdtErrorKind::InvalidLength`] if the property's value is + /// not 8 bytes long. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::fdt::Fdt; + /// # let dtb = include_bytes!("../../tests/dtb/test_props.dtb"); + /// let fdt = Fdt::new(dtb).unwrap(); + /// let node = fdt.find_node("/test-props").unwrap().unwrap(); + /// let prop = node.property("u64-prop").unwrap().unwrap(); + /// assert_eq!(prop.as_u64().unwrap(), 0x1122334455667788); + /// ``` + pub fn as_u64(&self) -> Result { + big_endian::U64::ref_from_bytes(self.value) + .map(|val| val.get()) + .map_err(|_e| FdtError::new(FdtErrorKind::InvalidLength, self.value_offset)) + } + + /// Returns the value of this property as a string. + /// + /// # Errors + /// + /// Returns an [`FdtErrorKind::InvalidString`] if the property's value is + /// not a null-terminated string or contains invalid UTF-8. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::fdt::Fdt; + /// # let dtb = include_bytes!("../../tests/dtb/test_props.dtb"); + /// let fdt = Fdt::new(dtb).unwrap(); + /// let node = fdt.find_node("/test-props").unwrap().unwrap(); + /// let prop = node.property("str-prop").unwrap().unwrap(); + /// assert_eq!(prop.as_str().unwrap(), "hello world"); + /// ``` + pub fn as_str(&self) -> Result<&'a str, FdtError> { + let cstr = CStr::from_bytes_with_nul(self.value) + .map_err(|_| FdtError::new(FdtErrorKind::InvalidString, self.value_offset))?; + cstr.to_str() + .map_err(|_| FdtError::new(FdtErrorKind::InvalidString, self.value_offset)) + } + + /// Returns an iterator over the strings in this property. + /// + /// # Examples + /// + /// ``` + /// # use dtoolkit::fdt::Fdt; + /// # let dtb = include_bytes!("../../tests/dtb/test_props.dtb"); + /// let fdt = Fdt::new(dtb).unwrap(); + /// let node = fdt.find_node("/test-props").unwrap().unwrap(); + /// let prop = node.property("str-list-prop").unwrap().unwrap(); + /// let mut str_list = prop.as_str_list(); + /// assert_eq!(str_list.next(), Some("first")); + /// assert_eq!(str_list.next(), Some("second")); + /// assert_eq!(str_list.next(), Some("third")); + /// assert_eq!(str_list.next(), None); + /// ``` + pub fn as_str_list(&self) -> impl Iterator { + FdtStringListIterator { value: self.value } + } +} + +/// An iterator over the properties of a device tree node. +pub(crate) enum FdtPropIter<'a> { + Start { fdt: &'a Fdt<'a>, offset: usize }, + Running { fdt: &'a Fdt<'a>, offset: usize }, + Error, +} + +impl<'a> Iterator for FdtPropIter<'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> FdtPropIter<'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::Prop => { + let len = match big_endian::U32::ref_from_prefix( + &fdt.data[*offset + FDT_TAGSIZE..], + ) { + Ok((val, _)) => val.get() as usize, + Err(_) => { + return Some(Err(FdtError::new(FdtErrorKind::InvalidLength, *offset))); + } + }; + let nameoff = match big_endian::U32::ref_from_prefix( + &fdt.data[*offset + 2 * FDT_TAGSIZE..], + ) { + Ok((val, _)) => val.get() as usize, + Err(_) => { + return Some(Err(FdtError::new(FdtErrorKind::InvalidLength, *offset))); + } + }; + let prop_offset = *offset + 3 * FDT_TAGSIZE; + *offset = Fdt::align_tag_offset(prop_offset + len); + let name = match fdt.string(nameoff) { + Ok(name) => name, + Err(e) => return Some(Err(e)), + }; + let value = fdt.data.get(prop_offset..prop_offset + len)?; + return Some(Ok(FdtProperty { + name, + value, + value_offset: prop_offset, + })); + } + FdtToken::Nop => *offset += FDT_TAGSIZE, + _ => return None, + } + } + } +} + +struct FdtStringListIterator<'a> { + value: &'a [u8], +} + +impl<'a> Iterator for FdtStringListIterator<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + if self.value.is_empty() { + return None; + } + let cstr = CStr::from_bytes_until_nul(self.value).ok()?; + let s = cstr.to_str().ok()?; + self.value = &self.value[s.len() + 1..]; + Some(s) + } +} diff --git a/tests/dtb/test_children_nested.dtb b/tests/dtb/test_children_nested.dtb new file mode 100644 index 0000000..4cd0a3e Binary files /dev/null and b/tests/dtb/test_children_nested.dtb differ diff --git a/tests/dtb/test_props.dtb b/tests/dtb/test_props.dtb new file mode 100644 index 0000000..7eb1afd Binary files /dev/null and b/tests/dtb/test_props.dtb differ diff --git a/tests/dts/test_children_nested.dts b/tests/dts/test_children_nested.dts new file mode 100644 index 0000000..83a1b31 --- /dev/null +++ b/tests/dts/test_children_nested.dts @@ -0,0 +1,15 @@ +/dts-v1/; + +/ { + child1 { + prop1 = <0x02>; + + child2 { + prop2 = <0x02>; + }; + }; + + child3 { + prop3 = <0x02>; + }; +}; diff --git a/tests/dts/test_props.dts b/tests/dts/test_props.dts new file mode 100644 index 0000000..779a31b --- /dev/null +++ b/tests/dts/test_props.dts @@ -0,0 +1,13 @@ +/dts-v1/; + +/ { + #address-cells = <2>; + #size-cells = <2>; + + test-props { + u32-prop = <0x12345678>; + u64-prop = <0x11223344 0x55667788>; + str-prop = "hello world"; + str-list-prop = "first", "second", "third"; + }; +}; diff --git a/tests/fdt.rs b/tests/fdt.rs index 4a7cc4d..66e06a0 100644 --- a/tests/fdt.rs +++ b/tests/fdt.rs @@ -24,6 +24,58 @@ fn read_child_nodes() { assert!(children.next().is_none()); } +#[test] +fn read_prop_values() { + let dtb = include_bytes!("dtb/test_props.dtb"); + let fdt = Fdt::new(dtb).unwrap(); + let root = fdt.root().unwrap(); + let mut children = root.children(); + let node = children.next().unwrap().unwrap(); + assert_eq!(node.name().unwrap(), "test-props"); + + let mut props = node.properties(); + + let prop = props.next().unwrap().unwrap(); + assert_eq!(prop.name(), "u32-prop"); + assert_eq!(prop.as_u32().unwrap(), 0x1234_5678); + + let prop = props.next().unwrap().unwrap(); + assert_eq!(prop.name(), "u64-prop"); + assert_eq!(prop.as_u64().unwrap(), 0x1122_3344_5566_7788); + + let prop = props.next().unwrap().unwrap(); + assert_eq!(prop.name(), "str-prop"); + assert_eq!(prop.as_str().unwrap(), "hello world"); + + let prop = props.next().unwrap().unwrap(); + assert_eq!(prop.name(), "str-list-prop"); + let mut str_list = prop.as_str_list(); + assert_eq!(str_list.next(), Some("first")); + assert_eq!(str_list.next(), Some("second")); + assert_eq!(str_list.next(), Some("third")); + assert_eq!(str_list.next(), None); + + assert!(props.next().is_none()); +} + +#[test] +fn get_property_by_name() { + let dtb = include_bytes!("dtb/test_props.dtb"); + let fdt = Fdt::new(dtb).unwrap(); + let root = fdt.root().unwrap(); + let node = root.child("test-props").unwrap().unwrap(); + + let prop = node.property("u32-prop").unwrap().unwrap(); + assert_eq!(prop.name(), "u32-prop"); + assert_eq!(prop.as_u32().unwrap(), 0x1234_5678); + + let prop = node.property("str-prop").unwrap().unwrap(); + assert_eq!(prop.name(), "str-prop"); + assert_eq!(prop.as_str().unwrap(), "hello world"); + + assert!(node.property("non-existent-prop").unwrap().is_none()); +} + #[test] fn get_child_by_name() { let dtb = include_bytes!("dtb/test_children.dtb"); @@ -39,6 +91,31 @@ fn get_child_by_name() { assert!(root.child("non-existent-child").unwrap().is_none()); } +#[test] +fn children_nested() { + let dtb = include_bytes!("dtb/test_children_nested.dtb"); + let fdt = Fdt::new(dtb).unwrap(); + let root = fdt.root().unwrap(); + + for child in root.children() { + println!("{}", child.unwrap().name().unwrap()); + } + + let children_names: Vec<_> = root + .children() + .map(|child| child.unwrap().name().unwrap()) + .collect(); + assert_eq!(children_names, vec!["child1", "child3"]); + + let child1 = root.child("child1").unwrap().unwrap(); + let child2 = child1.child("child2").unwrap().unwrap(); + let nested_properties: Vec<_> = child2 + .properties() + .map(|prop| prop.unwrap().name().to_owned()) + .collect(); + assert_eq!(nested_properties, vec!["prop2"]); +} + #[test] fn find_node_by_path() { let dtb = include_bytes!("dtb/test_traversal.dtb");