Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ rustdoc-args = ["--cfg", "docsrs"]
default = []
alloc = []
arrayvec07 = ["dep:arrayvec"]
overlay = ["write"]

Copy link
Copy Markdown
Collaborator

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 write feature, and hence alloc?

Copy link
Copy Markdown
Collaborator

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, at least alloc is needed for the use of String to provide more useful errors. The actual feature of applying overlays (in #60) is implemented for DeviceTree only (for now), so it's a bit forward-looking - but the point is, in the final form, write will be needed anyways. I can change this to alloc in this PR, though.

write = ["alloc", "dep:indexmap", "dep:twox-hash"]

[dependencies]
Expand Down
45 changes: 45 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why write rather than overlay?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - yes, this should be overlay.

#[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),
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
242 changes: 242 additions & 0 deletions src/overlay.rs
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)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

@m4tx m4tx Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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));
}
}
Binary file added tests/dtb/overlay_dependent_base.dtb
Binary file not shown.
Binary file added tests/dtb/overlay_dependent_overlay.dtb
Binary file not shown.
Binary file added tests/dtb/overlay_external_symbols_base.dtb
Binary file not shown.
Binary file added tests/dtb/overlay_external_symbols_overlay.dtb
Binary file not shown.
Binary file added tests/dtb/overlay_local_fixups_base.dtb
Binary file not shown.
Binary file added tests/dtb/overlay_local_fixups_overlay.dtb
Binary file not shown.
Binary file added tests/dtb/overlay_target_path_base.dtb
Binary file not shown.
Binary file added tests/dtb/overlay_target_path_overlay.dtb
Binary file not shown.
3 changes: 3 additions & 0 deletions tests/dts/overlay_dependent_base.dts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/dts-v1/;
/ {
};
13 changes: 13 additions & 0 deletions tests/dts/overlay_dependent_overlay.dts
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";
};
6 changes: 6 additions & 0 deletions tests/dts/overlay_external_symbols_base.dts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/dts-v1/;

/ {
soc: soc {
};
};
8 changes: 8 additions & 0 deletions tests/dts/overlay_external_symbols_overlay.dts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/dts-v1/;
/plugin/;

&soc {
uart0: uart@1000 {
status = "okay";
};
};
6 changes: 6 additions & 0 deletions tests/dts/overlay_local_fixups_base.dts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/dts-v1/;

/ {
clk: clock-controller@100 {
};
};
8 changes: 8 additions & 0 deletions tests/dts/overlay_local_fixups_overlay.dts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/dts-v1/;
/plugin/;

&{/} {
ovl_clk: dev@200 {
clocks = <&ovl_clk>;
};
};
7 changes: 7 additions & 0 deletions tests/dts/overlay_target_path_base.dts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/dts-v1/;

/ {
soc {
status = "disabled";
};
};
9 changes: 9 additions & 0 deletions tests/dts/overlay_target_path_overlay.dts
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 {
};
};
20 changes: 20 additions & 0 deletions tests/generate_dtbs.sh
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
Loading