From 21854f052025f53b8ca31e9a5bf28738ae5f9810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Tue, 7 Jul 2026 10:08:08 +0000 Subject: [PATCH 1/3] feat(model): add `DeviceTree::find_node` read-only lookup --- src/model.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/model.rs b/src/model.rs index 0a38d44..00e842b 100644 --- a/src/model.rs +++ b/src/model.rs @@ -23,6 +23,7 @@ use core::fmt::Display; pub use node::{DeviceTreeNode, DeviceTreeNodeBuilder}; pub use property::DeviceTreeProperty; +use crate::Node; use crate::fdt::Fdt; use crate::memreserve::MemoryReservation; @@ -89,6 +90,40 @@ impl DeviceTree { } } + /// Finds a node by its path and returns a 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::Node; + /// use dtoolkit::model::{DeviceTree, DeviceTreeNode}; + /// + /// let mut tree = DeviceTree::new(); + /// tree.root.add_child(DeviceTreeNode::new("child").unwrap()); + /// let child = tree.find_node("/child").unwrap(); + /// assert_eq!(child.name(), "child"); + /// ``` + #[must_use] + pub fn find_node(&self, path: &str) -> Option<&DeviceTreeNode> { + if !path.starts_with('/') { + return None; + } + let mut current_node = &self.root; + if path == "/" { + return Some(current_node); + } + for component in path.split('/').filter(|s| !s.is_empty()) { + current_node = current_node.child(component)?; + } + Some(current_node) + } + /// Finds a node by its path and returns a mutable reference to it. /// /// # Performance From fd77afa778f0100506503bab1295f0cff8042fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Wed, 15 Jul 2026 13:32:43 +0000 Subject: [PATCH 2/3] feat(model): add DeviceTreeNode::add_child_mut --- src/model/node.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/model/node.rs b/src/model/node.rs index 06b571f..3b5b5c8 100644 --- a/src/model/node.rs +++ b/src/model/node.rs @@ -392,6 +392,36 @@ impl DeviceTreeNode { self.children.insert(child.name.clone(), child); } + /// Adds a child to this node, returning a reference to it. + /// + /// # Performance + /// + /// This is a constant-time operation. + /// + /// # Examples + /// + /// ``` + /// use dtoolkit::model::{DeviceTreeNode, DeviceTreeProperty}; + /// use dtoolkit::{Node, Property}; + /// + /// let mut node = DeviceTreeNode::new("my-node").unwrap(); + /// let mut child = node.add_child_mut(DeviceTreeNode::new("child").unwrap()); + /// child.add_property(DeviceTreeProperty::new("test", "value").unwrap()); + /// assert_eq!( + /// (&node) + /// .child("child") + /// .unwrap() + /// .property("test") + /// .unwrap() + /// .value(), + /// b"value" + /// ); + /// ``` + #[must_use] + pub fn add_child_mut(&mut self, child: DeviceTreeNode) -> &mut DeviceTreeNode { + self.children.entry(child.name.clone()).or_insert(child) + } + /// Removes a child from this node by its name. /// /// # Performance From 9d12e86fe7e041e0498337856411f6a94e0d65f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Fri, 10 Jul 2026 09:40:12 +0000 Subject: [PATCH 3/3] feat(overlay): add read-only DT overlay inspection capabilities --- Cargo.toml | 1 + src/error.rs | 45 ++++ src/lib.rs | 2 + src/overlay.rs | 242 ++++++++++++++++++ tests/dtb/overlay_dependent_base.dtb | Bin 0 -> 72 bytes tests/dtb/overlay_dependent_overlay.dtb | Bin 0 -> 304 bytes tests/dtb/overlay_external_symbols_base.dtb | Bin 0 -> 152 bytes .../dtb/overlay_external_symbols_overlay.dtb | Bin 0 -> 340 bytes tests/dtb/overlay_local_fixups_base.dtb | Bin 0 -> 188 bytes tests/dtb/overlay_local_fixups_overlay.dtb | Bin 0 -> 375 bytes tests/dtb/overlay_target_path_base.dtb | Bin 0 -> 115 bytes tests/dtb/overlay_target_path_overlay.dtb | Bin 0 -> 216 bytes tests/dts/overlay_dependent_base.dts | 3 + tests/dts/overlay_dependent_overlay.dts | 13 + tests/dts/overlay_external_symbols_base.dts | 6 + .../dts/overlay_external_symbols_overlay.dts | 8 + tests/dts/overlay_local_fixups_base.dts | 6 + tests/dts/overlay_local_fixups_overlay.dts | 8 + tests/dts/overlay_target_path_base.dts | 7 + tests/dts/overlay_target_path_overlay.dts | 9 + tests/generate_dtbs.sh | 20 ++ 21 files changed, 370 insertions(+) create mode 100644 src/overlay.rs create mode 100644 tests/dtb/overlay_dependent_base.dtb create mode 100644 tests/dtb/overlay_dependent_overlay.dtb create mode 100644 tests/dtb/overlay_external_symbols_base.dtb create mode 100644 tests/dtb/overlay_external_symbols_overlay.dtb create mode 100644 tests/dtb/overlay_local_fixups_base.dtb create mode 100644 tests/dtb/overlay_local_fixups_overlay.dtb create mode 100644 tests/dtb/overlay_target_path_base.dtb create mode 100644 tests/dtb/overlay_target_path_overlay.dtb create mode 100644 tests/dts/overlay_dependent_base.dts create mode 100644 tests/dts/overlay_dependent_overlay.dts create mode 100644 tests/dts/overlay_external_symbols_base.dts create mode 100644 tests/dts/overlay_external_symbols_overlay.dts create mode 100644 tests/dts/overlay_local_fixups_base.dts create mode 100644 tests/dts/overlay_local_fixups_overlay.dts create mode 100644 tests/dts/overlay_target_path_base.dts create mode 100644 tests/dts/overlay_target_path_overlay.dts create mode 100755 tests/generate_dtbs.sh diff --git a/Cargo.toml b/Cargo.toml index 41dca67..dd109c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ rustdoc-args = ["--cfg", "docsrs"] default = [] alloc = [] arrayvec07 = ["dep:arrayvec"] +overlay = ["write"] write = ["alloc", "dep:indexmap", "dep:twox-hash"] [dependencies] diff --git a/src/error.rs b/src/error.rs index e22614d..590114f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -166,3 +166,48 @@ pub enum FdtMutError { #[error("buffer resize failed: {0}")] Resize(#[from] BufferError), } + +/// An error that can occur when inspecting or applying a device tree overlay. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Error)] +#[cfg(feature = "write")] +#[non_exhaustive] +pub enum OverlayError { + /// An error occurred when parsing the device tree blob or header. + #[error("FDT parse error: {0}")] + Parse(#[from] FdtParseError), + /// An error occurred when accessing or converting a property value. + #[error("property conversion error: {0}")] + Property(#[from] PropertyError), + /// A fragment target property (`target` or `target-path`) is missing or + /// invalid. + #[error("fragment target is missing or invalid")] + InvalidFragmentTarget, + /// A phandle value overflowed 32-bit space during renumbering or collided + /// with a reserved value. + #[error( + "phandle value overflowed u32 space during renumbering or collided with reserved value" + )] + PhandleOverflow, + /// A location string in `/__fixups__` has an invalid format or offset. + #[error("invalid fixup location string")] + InvalidFixupLocation, + /// An external symbol referenced in `/__fixups__` could not be resolved in + /// the base tree. + #[error("unresolved symbol referenced in __fixups__: '{0}'")] + UnresolvedSymbol(String), + /// A target node referenced by a fragment could not be found in the base + /// tree. + #[error("target node not found in base tree: '{0}'")] + TargetNotFound(String), + /// A local fixup offset was out of bounds for the property value. + #[error("local fixup offset {offset} out of bounds for property '{prop}'")] + InvalidLocalFixup { + /// The property name. + prop: String, + /// The invalid byte offset. + offset: usize, + }, + /// A model error occurred during overlay application. + #[error("model error: {0}")] + Model(#[from] ModelError), +} diff --git a/src/lib.rs b/src/lib.rs index d7e5e74..3b7d4bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,8 @@ pub mod fdt_mut; pub mod memreserve; #[cfg(feature = "write")] pub mod model; +#[cfg(feature = "overlay")] +mod overlay; pub mod standard; mod util; mod validate; diff --git a/src/overlay.rs b/src/overlay.rs new file mode 100644 index 0000000..03a1c91 --- /dev/null +++ b/src/overlay.rs @@ -0,0 +1,242 @@ +// Copyright 2026 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. + +//! Helper types and methods for inspecting Device Tree Overlays (DTBOs). +//! +//! This module is modeled after the [`standard`](crate::standard) module, +//! providing generic wrappers over any type implementing [`Node`] so that +//! overlay inspection works uniformly across both read-only (`FdtNode`) and +//! in-memory (`DeviceTreeNode`) representations. + +#![allow(unused)] + +use core::ops::Deref; + +use crate::error::OverlayError; +use crate::{Node, Property}; + +pub(crate) const NODE_SYMBOLS: &str = "__symbols__"; +pub(crate) const NODE_FIXUPS: &str = "__fixups__"; +pub(crate) const NODE_LOCAL_FIXUPS: &str = "__local_fixups__"; +pub(crate) const PHANDLE_PROPS: [&str; 2] = ["phandle", "linux,phandle"]; + +/// Typed wrapper for a device tree overlay root node. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Overlay { + pub(crate) node: N, +} + +impl Overlay { + /// Returns an iterator over all overlay fragments in the DTBO. + pub(crate) fn fragments(&self) -> impl Iterator>> { + self.node.children().filter_map(|node| { + if node.name_without_address().as_ref() == "fragment" { + Some(Fragment { node }) + } else { + None + } + }) + } +} + +/// An overlay fragment within a DTBO blob or tree. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Fragment { + pub(crate) node: N, +} + +impl Deref for Fragment { + type Target = N; + + fn deref(&self) -> &Self::Target { + &self.node + } +} + +impl Fragment { + /// Returns the target of this fragment (`target` phandle or `target-path`). + /// + /// # Errors + /// + /// Returns [`OverlayError::InvalidFragmentTarget`] if neither property is + /// present or if both are present. Returns [`OverlayError::Property`] if + /// the target property cannot be converted to a u32 or string. + pub(crate) fn target( + &self, + ) -> Result as Property>::Str>, OverlayError> { + let target = self.node.property("target"); + let target_path = self.node.property("target-path"); + + match (target, target_path) { + (Some(prop), None) => Ok(FragmentTarget::Phandle(prop.as_u32()?)), + (None, Some(prop)) => Ok(FragmentTarget::Path(prop.as_str()?)), + _ => Err(OverlayError::InvalidFragmentTarget), + } + } +} + +/// The target of an overlay fragment in the base tree. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum FragmentTarget { + /// A 32-bit phandle pointing to a target node in the base tree. + Phandle(u32), + /// An absolute device tree path or alias/symbol pointing to a target node. + Path(S), +} + +/// A parsed location string from a `/__fixups__` property value. +/// +/// Each location string specifies where a target phandle needs to be patched +/// in the format `"/path/to/node:property_name:offset"`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct FixupLocation<'a> { + /// The path to the target node inside the overlay fragment. + pub(crate) node_path: &'a str, + /// The property name inside the target node. + pub(crate) property_name: &'a str, + /// The byte offset within the property value where the 32-bit big-endian + /// phandle resides. + pub(crate) offset: usize, +} + +impl<'a> FixupLocation<'a> { + /// Parses a location string from `/__fixups__`. + /// + /// # Errors + /// + /// Returns [`OverlayError::InvalidFixupLocation`] if the string does not + /// have exactly two colons separating non-empty path/property + /// components and a 4-byte aligned numeric offset. + pub(crate) fn parse(raw: &'a str) -> Result { + let mut parts = raw.rsplitn(3, ':'); + let offset_str = parts.next().ok_or(OverlayError::InvalidFixupLocation)?; + let property_name = parts.next().ok_or(OverlayError::InvalidFixupLocation)?; + let node_path = parts.next().ok_or(OverlayError::InvalidFixupLocation)?; + + if node_path.is_empty() || property_name.is_empty() { + return Err(OverlayError::InvalidFixupLocation); + } + + let offset = offset_str + .parse::() + .map_err(|_| OverlayError::InvalidFixupLocation)?; + if offset % 4 != 0 { + return Err(OverlayError::InvalidFixupLocation); + } + + Ok(Self { + node_path, + property_name, + offset, + }) + } +} + +/// Returns the phandle of a node, if present. +pub(crate) fn get_phandle(node: &N) -> Option { + for prop in PHANDLE_PROPS { + if let Some(p) = node.property(prop) + && let Ok(val) = p.as_u32() + { + return Some(val); + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fdt::Fdt; + use crate::model::{DeviceTreeNode, DeviceTreeProperty}; + + #[test] + fn test_parse_fixup_location() { + let loc = FixupLocation::parse("/fragment@0/__overlay__/dev@1000:clocks:0").unwrap(); + assert_eq!(loc.node_path, "/fragment@0/__overlay__/dev@1000"); + assert_eq!(loc.property_name, "clocks"); + assert_eq!(loc.offset, 0); + + assert_eq!( + FixupLocation::parse("invalid").unwrap_err(), + OverlayError::InvalidFixupLocation + ); + assert_eq!( + FixupLocation::parse("/path:prop:2").unwrap_err(), + OverlayError::InvalidFixupLocation + ); + } + + #[test] + fn read_overlay_fragments_and_targets() { + let overlay_dtb = include_bytes!("../tests/dtb/overlay_target_path_overlay.dtb"); + let overlay_fdt = Fdt::new(overlay_dtb).unwrap(); + let overlay = Overlay { + node: overlay_fdt.root(), + }; + + let mut fragments = overlay.fragments(); + let fragment = fragments.next().expect("Expected a fragment"); + assert!(fragments.next().is_none()); + + let target = fragment.target().unwrap(); + assert_eq!(target, FragmentTarget::Path("/soc")); + } + + #[test] + fn read_overlay_fixups() { + let overlay_dtb = include_bytes!("../tests/dtb/overlay_local_fixups_overlay.dtb"); + let overlay_fdt = Fdt::new(overlay_dtb).unwrap(); + let overlay = Overlay { + node: overlay_fdt.root(), + }; + + assert!(overlay.node.child(NODE_LOCAL_FIXUPS).is_some()); + + let mut fragments = overlay.fragments(); + let fragment = fragments.next().expect("Expected a fragment"); + let target = fragment.target().unwrap(); + assert_eq!(target, FragmentTarget::Path("/")); + } + + #[test] + fn read_overlay_symbols_and_external_fixups() { + let overlay_dtb = include_bytes!("../tests/dtb/overlay_external_symbols_overlay.dtb"); + let overlay_fdt = Fdt::new(overlay_dtb).unwrap(); + let overlay = Overlay { + node: overlay_fdt.root(), + }; + + assert!(overlay.node.child(NODE_SYMBOLS).is_some()); + assert!(overlay.node.child(NODE_FIXUPS).is_some()); + assert!(overlay.node.child(NODE_LOCAL_FIXUPS).is_none()); + + let mut fragments = overlay.fragments(); + let fragment = fragments.next().expect("Expected a fragment"); + let target = fragment.target().unwrap(); + // This overlay uses a phandle target pointing to a resolved symbol + assert!(matches!(target, FragmentTarget::Phandle(_))); + } + + #[test] + fn test_get_phandle() { + let mut node = DeviceTreeNode::new("test").unwrap(); + assert_eq!(get_phandle(&node), None); + + node.add_property( + DeviceTreeProperty::new("linux,phandle", 42u32.to_be_bytes().to_vec()).unwrap(), + ); + assert_eq!(get_phandle(&node), Some(42)); + + // phandle should take precedence if both are present + node.add_property( + DeviceTreeProperty::new("phandle", 100u32.to_be_bytes().to_vec()).unwrap(), + ); + assert_eq!(get_phandle(&node), Some(100)); + } +} diff --git a/tests/dtb/overlay_dependent_base.dtb b/tests/dtb/overlay_dependent_base.dtb new file mode 100644 index 0000000000000000000000000000000000000000..90ef5c00243b04f4aa3f812b89d5b37c63be09f2 GIT binary patch literal 72 mcmcb>`|m9S1A_+;TR>?IAT0>Q0zeD{$ZVJxBb31eq&Wct_yhI; literal 0 HcmV?d00001 diff --git a/tests/dtb/overlay_dependent_overlay.dtb b/tests/dtb/overlay_dependent_overlay.dtb new file mode 100644 index 0000000000000000000000000000000000000000..c3577e523a8c3e623c527dd156c48571fe5c81c6 GIT binary patch literal 304 zcmZ9GO$x#=5QV4yEQAV*OScMcN?f}1CL!9`f@xBksPvXDyoOg0-$cdgz?bPu!_uDL<*-Vmhjs9GK%cs2ktz<#XT1=_x?cFy>hHveCOx%eI>I{pIQ;?MV^HwvmL XbjWHIO7cO4lc%~=HaD8EBIBt~8P+nc literal 0 HcmV?d00001 diff --git a/tests/dtb/overlay_external_symbols_base.dtb b/tests/dtb/overlay_external_symbols_base.dtb new file mode 100644 index 0000000000000000000000000000000000000000..33463e1adf3e013251b5180d83769edbac0a79b0 GIT binary patch literal 152 zcmcb>`|m9S1H%j;wgBQDAl3k4K_C_YVi4c~;t((km0*N&8H@9iK}=>KW`T(TX(k|M qjE^s_%uUMADUOc^NrBX`0x^d^R1=U5QU}7E1sREXDLJVOASnP+#1K~i literal 0 HcmV?d00001 diff --git a/tests/dtb/overlay_external_symbols_overlay.dtb b/tests/dtb/overlay_external_symbols_overlay.dtb new file mode 100644 index 0000000000000000000000000000000000000000..eaad3133a974864c80dcf338c91b265db84c177c GIT binary patch literal 340 zcmZ8b%L>9U5Zw4cJSck5lLztStr0v(PyGn*A!=I1wkb&}_AmVr|3K7TTeWmxvYTO; z+1ucG2Vh$O*x+2_Tq4KF5t1-RK4@uxReu&586_%p0skm%d^@zg5IC3+`l3uM3n55m zxil_VabDVi{9R;EpJhP?#5ZGzJN2|`#5`LhJ006HkGu=y)cYUe)jGXKA7x=ic)I3U o74hktpgyZ*_)eNgIlnsdcSFC_!8+-33)xYohgbo%aIkt0Z$ZL5*8l(j literal 0 HcmV?d00001 diff --git a/tests/dtb/overlay_local_fixups_base.dtb b/tests/dtb/overlay_local_fixups_base.dtb new file mode 100644 index 0000000000000000000000000000000000000000..1c223ee21df6f830a22fdb006562ba405f93da2d GIT binary patch literal 188 zcmcb>`|m9S1H&F5wgBP{K&%18f=jW9a<>%z2 z7C9Ii7(nHifiw$D9gt=MV#fIR;>z5l{G8(Wc#srGjTjJf=;JmBqz{BS3o;V(QgTum Il5?^d084@#+W-In literal 0 HcmV?d00001 diff --git a/tests/dtb/overlay_local_fixups_overlay.dtb b/tests/dtb/overlay_local_fixups_overlay.dtb new file mode 100644 index 0000000000000000000000000000000000000000..1b4e0395b4f4b3f3014a126889df5e5a3079680c GIT binary patch literal 375 zcma)0K@Ng25ZtO46HPQ8^w8Wa<3FN~I|@t)Q>jABS_o2D zs4D;HIWu`Dh#BdKj;~ryIUO*ktka9pWs^hREn?041NdMUAMBwFoYy9hT12TuF$B0WDIt$#7HS2i(jw3ad;!-FJY@g? literal 0 HcmV?d00001 diff --git a/tests/dtb/overlay_target_path_base.dtb b/tests/dtb/overlay_target_path_base.dtb new file mode 100644 index 0000000000000000000000000000000000000000..f6b3b98150d79bd86f9cc81bba94242670a07eb6 GIT binary patch literal 115 zcmcb>`|m9S14A(oTL5tm5NiOjAP@@xF$k~&u?d)kN-#pXjK%rMASN>qbAlKtnZ=1o VIjJciHV`mDF=uf}Vo7N+0|0u`3!nf1 literal 0 HcmV?d00001 diff --git a/tests/dtb/overlay_target_path_overlay.dtb b/tests/dtb/overlay_target_path_overlay.dtb new file mode 100644 index 0000000000000000000000000000000000000000..c305c233ca857ac10b3d9be6ca4a0a642f2ffe62 GIT binary patch literal 216 zcmcb>`|m9S1H%m; + }; +}; diff --git a/tests/dts/overlay_target_path_base.dts b/tests/dts/overlay_target_path_base.dts new file mode 100644 index 0000000..bf309e5 --- /dev/null +++ b/tests/dts/overlay_target_path_base.dts @@ -0,0 +1,7 @@ +/dts-v1/; + +/ { + soc { + status = "disabled"; + }; +}; diff --git a/tests/dts/overlay_target_path_overlay.dts b/tests/dts/overlay_target_path_overlay.dts new file mode 100644 index 0000000..d919e28 --- /dev/null +++ b/tests/dts/overlay_target_path_overlay.dts @@ -0,0 +1,9 @@ +/dts-v1/; +/plugin/; + +&{/soc} { + status = "okay"; + new-prop = "foo"; + serial@1000 { + }; +}; diff --git a/tests/generate_dtbs.sh b/tests/generate_dtbs.sh new file mode 100755 index 0000000..db1bc56 --- /dev/null +++ b/tests/generate_dtbs.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Copyright 2026 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. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DTS_DIR="${SCRIPT_DIR}/dts" +DTB_DIR="${SCRIPT_DIR}/dtb" + +for dts in "${DTS_DIR}"/*.dts; do + dtb="${DTB_DIR}/$(basename "${dts}" .dts).dtb" + echo "Compiling $(basename "${dts}") -> $(basename "${dtb}")" + dtc -@ -I dts -O dtb -o "${dtb}" "${dts}" +done