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 0000000..90ef5c0 Binary files /dev/null and b/tests/dtb/overlay_dependent_base.dtb differ diff --git a/tests/dtb/overlay_dependent_overlay.dtb b/tests/dtb/overlay_dependent_overlay.dtb new file mode 100644 index 0000000..c3577e5 Binary files /dev/null and b/tests/dtb/overlay_dependent_overlay.dtb differ diff --git a/tests/dtb/overlay_external_symbols_base.dtb b/tests/dtb/overlay_external_symbols_base.dtb new file mode 100644 index 0000000..33463e1 Binary files /dev/null and b/tests/dtb/overlay_external_symbols_base.dtb differ diff --git a/tests/dtb/overlay_external_symbols_overlay.dtb b/tests/dtb/overlay_external_symbols_overlay.dtb new file mode 100644 index 0000000..eaad313 Binary files /dev/null and b/tests/dtb/overlay_external_symbols_overlay.dtb differ diff --git a/tests/dtb/overlay_local_fixups_base.dtb b/tests/dtb/overlay_local_fixups_base.dtb new file mode 100644 index 0000000..1c223ee Binary files /dev/null and b/tests/dtb/overlay_local_fixups_base.dtb differ diff --git a/tests/dtb/overlay_local_fixups_overlay.dtb b/tests/dtb/overlay_local_fixups_overlay.dtb new file mode 100644 index 0000000..1b4e039 Binary files /dev/null and b/tests/dtb/overlay_local_fixups_overlay.dtb differ diff --git a/tests/dtb/overlay_target_path_base.dtb b/tests/dtb/overlay_target_path_base.dtb new file mode 100644 index 0000000..f6b3b98 Binary files /dev/null and b/tests/dtb/overlay_target_path_base.dtb differ diff --git a/tests/dtb/overlay_target_path_overlay.dtb b/tests/dtb/overlay_target_path_overlay.dtb new file mode 100644 index 0000000..c305c23 Binary files /dev/null and b/tests/dtb/overlay_target_path_overlay.dtb differ diff --git a/tests/dts/overlay_dependent_base.dts b/tests/dts/overlay_dependent_base.dts new file mode 100644 index 0000000..0d174ca --- /dev/null +++ b/tests/dts/overlay_dependent_base.dts @@ -0,0 +1,3 @@ +/dts-v1/; +/ { +}; diff --git a/tests/dts/overlay_dependent_overlay.dts b/tests/dts/overlay_dependent_overlay.dts new file mode 100644 index 0000000..561146e --- /dev/null +++ b/tests/dts/overlay_dependent_overlay.dts @@ -0,0 +1,13 @@ +/dts-v1/; +/plugin/; + +&{/} { + soc { + new_node: new-node { + }; + }; +}; + +&new_node { + status = "okay"; +}; diff --git a/tests/dts/overlay_external_symbols_base.dts b/tests/dts/overlay_external_symbols_base.dts new file mode 100644 index 0000000..a39ed3d --- /dev/null +++ b/tests/dts/overlay_external_symbols_base.dts @@ -0,0 +1,6 @@ +/dts-v1/; + +/ { + soc: soc { + }; +}; diff --git a/tests/dts/overlay_external_symbols_overlay.dts b/tests/dts/overlay_external_symbols_overlay.dts new file mode 100644 index 0000000..3aa779a --- /dev/null +++ b/tests/dts/overlay_external_symbols_overlay.dts @@ -0,0 +1,8 @@ +/dts-v1/; +/plugin/; + +&soc { + uart0: uart@1000 { + status = "okay"; + }; +}; diff --git a/tests/dts/overlay_local_fixups_base.dts b/tests/dts/overlay_local_fixups_base.dts new file mode 100644 index 0000000..5fc9420 --- /dev/null +++ b/tests/dts/overlay_local_fixups_base.dts @@ -0,0 +1,6 @@ +/dts-v1/; + +/ { + clk: clock-controller@100 { + }; +}; diff --git a/tests/dts/overlay_local_fixups_overlay.dts b/tests/dts/overlay_local_fixups_overlay.dts new file mode 100644 index 0000000..34a1f8d --- /dev/null +++ b/tests/dts/overlay_local_fixups_overlay.dts @@ -0,0 +1,8 @@ +/dts-v1/; +/plugin/; + +&{/} { + ovl_clk: dev@200 { + clocks = <&ovl_clk>; + }; +}; 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