-
Notifications
You must be signed in to change notification settings - Fork 1
feat(overlay): add read-only DT overlay inspection capabilities #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
21854f0
fd77afa
9d12e86
1e224b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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")] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch - yes, this should be |
||
| #[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), | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,242 @@ | ||
| // Copyright 2026 Google LLC | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or | ||
| // https://www.apache.org/licenses/LICENSE-2.0> or the MIT license | ||
| // <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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)] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this needed?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, sorry - I should've made it clear that this is part of a bigger chain that is split to make the reviewing a bit easier (maybe eventually we'll get stacked PRs in GitHub, but for this needs to do). This read only part is later used in #60. |
||
|
|
||
| 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<N> { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any reason why this shouldn't be public? I guess it's not super common, but someone might want to use this crate to display an overlay in some sort of debug UI or something.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, the main reason could be that we promise some API that people might start relying on, on later on we might change or remove it. If you think it could be useful, though, I guess it's fine to make it public. |
||
| pub(crate) node: N, | ||
| } | ||
|
|
||
| impl<N: Node> Overlay<N> { | ||
| /// Returns an iterator over all overlay fragments in the DTBO. | ||
| pub(crate) fn fragments(&self) -> impl Iterator<Item = Fragment<N::Child<'_>>> { | ||
| 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<N> { | ||
| pub(crate) node: N, | ||
| } | ||
|
|
||
| impl<N> Deref for Fragment<N> { | ||
| type Target = N; | ||
|
|
||
| fn deref(&self) -> &Self::Target { | ||
| &self.node | ||
| } | ||
| } | ||
|
|
||
| impl<N: Node> Fragment<N> { | ||
| /// 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<FragmentTarget<<N::Property<'_> 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<S> { | ||
| /// 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<Self, OverlayError> { | ||
| 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::<usize>() | ||
| .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<N: Node>(node: &N) -> Option<u32> { | ||
| 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)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| /dts-v1/; | ||
| / { | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| /dts-v1/; | ||
| /plugin/; | ||
|
|
||
| &{/} { | ||
| soc { | ||
| new_node: new-node { | ||
| }; | ||
| }; | ||
| }; | ||
|
|
||
| &new_node { | ||
| status = "okay"; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| /dts-v1/; | ||
|
|
||
| / { | ||
| soc: soc { | ||
| }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| /dts-v1/; | ||
| /plugin/; | ||
|
|
||
| &soc { | ||
| uart0: uart@1000 { | ||
| status = "okay"; | ||
| }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| /dts-v1/; | ||
|
|
||
| / { | ||
| clk: clock-controller@100 { | ||
| }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| /dts-v1/; | ||
| /plugin/; | ||
|
|
||
| &{/} { | ||
| ovl_clk: dev@200 { | ||
| clocks = <&ovl_clk>; | ||
| }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /dts-v1/; | ||
|
|
||
| / { | ||
| soc { | ||
| status = "disabled"; | ||
| }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| /dts-v1/; | ||
| /plugin/; | ||
|
|
||
| &{/soc} { | ||
| status = "okay"; | ||
| new-prop = "foo"; | ||
| serial@1000 { | ||
| }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| #!/usr/bin/env bash | ||
| # Copyright 2026 Google LLC | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or | ||
| # https://www.apache.org/licenses/LICENSE-2.0> or the MIT license | ||
| # <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does read-only inspection of an overlay require the
writefeature, and hencealloc?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually for that matter, why does it need a separate feature at all? It doesn't add any extra dependencies, so why not enable it always?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, at least
allocis needed for the use ofStringto provide more useful errors. The actual feature of applying overlays (in #60) is implemented forDeviceTreeonly (for now), so it's a bit forward-looking - but the point is, in the final form,writewill be needed anyways. I can change this toallocin this PR, though.