Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
53 changes: 53 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,59 @@
//! The library is written purely in Rust and is `#![no_std]` compatible. If
//! you don't need the Device Tree manipulation functionality, the library is
//! also no-`alloc`-compatible.
//!
//! ## Read-Only API
//!
//! The read-only API is centered around the [`Fdt`](fdt::Fdt) struct, which
//! provides a safe, zero-copy view of an FDT blob. You can use this API
//! to traverse the device tree, inspect nodes and properties, and read
//! property values.
//!
//! Note that because the [`Fdt`](fdt::Fdt) struct is zero-copy, certains
//! operations such as node or lookups run in linear time. If you need to
//! perform these operations often and you can spare extra memory, it might be
//! beneficial to convert from [`Fdt`](fdt::Fdt) to
//! [`DeviceTree`](model::DeviceTree) first.
//!
//! ## Read-Write API
//!
//! The read-write API is centered around the [`DeviceTree`](model::DeviceTree)
//! struct, which provides a mutable, in-memory representation of a device tree.
//! You can use this API to create new device trees from scratch, modify
//! existing ones, and serialize them back to an FDT blob.
//!
//! Internally it is built upon hash maps, meaning that most lookup and
//! modification operations run in constant time.
//!
//! # Examples
//!
//! ```
//! use dtoolkit::fdt::Fdt;
//! use dtoolkit::model::{DeviceTree, DeviceTreeNode, DeviceTreeProperty};
//!
//! // Create a new device tree from scratch.
//! let mut tree = DeviceTree::new();
//!
//! // Add a child node to the root.
//! let child = DeviceTreeNode::builder("child")
//! .property(DeviceTreeProperty::new("my-property", "hello\0"))
//! .build();
//! tree.root.add_child(child);
//!
//! // Serialize the device tree to a DTB.
//! let dtb = tree.to_dtb();
//!
//! // Parse the DTB with the read-only API.
//! let fdt = Fdt::new(&dtb).unwrap();
//!
//! // Find the child node and read its property.
//! let child_node = fdt.find_node("/child").unwrap().unwrap();
//! let prop = child_node.property("my-property").unwrap().unwrap();
//! assert_eq!(prop.as_str().unwrap(), "hello");
//!
//! // Display the DTS
//! println!("{}", fdt);
//! ```
#![no_std]
#![warn(missing_docs, rustdoc::missing_crate_level_docs)]
Expand Down
21 changes: 21 additions & 0 deletions src/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
//! flattened device tree blob.

use alloc::vec::Vec;
use core::fmt::Display;

use crate::error::FdtError;
use crate::fdt::Fdt;
use crate::memreserve::MemoryReservation;
mod node;
mod property;
mod writer;
pub use node::{DeviceTreeNode, DeviceTreeNodeBuilder};
pub use property::DeviceTreeProperty;

Expand Down Expand Up @@ -85,6 +87,17 @@ impl DeviceTree {
})
}

/// Serializes the `DeviceTree` to a flattened device tree blob.
///
/// # Panics
///
/// This may panic if any of the lengths written to the DTB (block sizes,
/// property value length, etc.) exceed [`u32::MAX`].
#[must_use]
pub fn to_dtb(&self) -> Vec<u8> {
writer::to_bytes(self)
}

/// Finds a node by its path and returns a mutable reference to it.
///
/// # Performance
Expand Down Expand Up @@ -125,3 +138,11 @@ impl Default for DeviceTree {
Self::new()
}
}

impl Display for DeviceTree {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
Fdt::new(&self.to_dtb())
.expect("DeviceTree::to_dtb() should always generate a valid FDT")
.fmt(f)
}
}
159 changes: 159 additions & 0 deletions src/model/writer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Copyright 2025 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.

use alloc::borrow::ToOwned;
use alloc::collections::btree_map::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;

use zerocopy::IntoBytes;

use crate::fdt::{FDT_BEGIN_NODE, FDT_END, FDT_END_NODE, FDT_MAGIC, FDT_PROP, Fdt, FdtHeader};
use crate::memreserve::MemoryReservation;
use crate::model::{DeviceTree, DeviceTreeNode, DeviceTreeProperty};

// https://devicetree-specification.readthedocs.io/en/latest/chapter5-flattened-format.html#header
const LAST_VERSION: u32 = 17;
const LAST_COMP_VERSION: u32 = 16;

pub(crate) fn to_bytes(tree: &DeviceTree) -> Vec<u8> {
let memory_reservations = write_memory_reservations(&tree.memory_reservations);
let (struct_block, strings_block) = write_root(&tree.root);

let off_mem_rsvmap = size_of::<FdtHeader>();
let off_dt_struct = off_mem_rsvmap + memory_reservations.len();
let off_dt_strings = off_dt_struct + struct_block.len();
let totalsize = off_dt_strings + strings_block.len();

let mut dtb = Vec::new();

// Header
let header = FdtHeader {
magic: FDT_MAGIC.into(),
totalsize: u32::try_from(totalsize)
.expect("totalsize exceeds u32")
.into(),
off_dt_struct: u32::try_from(off_dt_struct)
.expect("off_dt_struct exceeds u32")
.into(),
off_dt_strings: u32::try_from(off_dt_strings)
.expect("off_dt_strings exceeds u32")
.into(),
off_mem_rsvmap: u32::try_from(off_mem_rsvmap)
.expect("off_mem_rsvmap exceeds u32")
.into(),
version: LAST_VERSION.into(),
last_comp_version: LAST_COMP_VERSION.into(),
boot_cpuid_phys: 0u32.into(),
size_dt_strings: u32::try_from(strings_block.len())
.expect("size_dt_strings exceeds u32")
.into(),
size_dt_struct: u32::try_from(struct_block.len())
.expect("size_dt_struct exceeds u32")
.into(),
};
dtb.extend_from_slice(header.as_bytes());
assert_eq!(
dtb.len(),
size_of::<FdtHeader>(),
"invalid header size after writing"
);

// Memory reservations block
dtb.extend_from_slice(&memory_reservations);

// Struct block
dtb.extend_from_slice(&struct_block);

// Strings block
dtb.extend_from_slice(&strings_block);

dtb
}

fn write_memory_reservations(reservations: &[MemoryReservation]) -> Vec<u8> {
let mut memory_reservations = Vec::new();
for reservation in reservations {
memory_reservations.extend_from_slice(&reservation.address().to_be_bytes());
memory_reservations.extend_from_slice(&reservation.size().to_be_bytes());
}
memory_reservations.extend_from_slice(&0u64.to_be_bytes());
memory_reservations.extend_from_slice(&0u64.to_be_bytes());
memory_reservations
}

fn write_root(root_node: &DeviceTreeNode) -> (Vec<u8>, Vec<u8>) {
let mut struct_block = Vec::new();
let mut strings_block = Vec::new();
let mut string_map = BTreeMap::new();

write_node(
&mut struct_block,
&mut strings_block,
&mut string_map,
root_node,
);
struct_block.extend_from_slice(&FDT_END.to_be_bytes());

(struct_block, strings_block)
}

fn write_node(
struct_block: &mut Vec<u8>,
strings_block: &mut Vec<u8>,
string_map: &mut BTreeMap<String, u32>,
node: &DeviceTreeNode,
) {
struct_block.extend_from_slice(&FDT_BEGIN_NODE.to_be_bytes());
struct_block.extend_from_slice(node.name().as_bytes());
struct_block.push(0);
align(struct_block);

for prop in node.properties() {
write_prop(struct_block, strings_block, string_map, prop);
}

for child in node.children() {
write_node(struct_block, strings_block, string_map, child);
}

struct_block.extend_from_slice(&FDT_END_NODE.to_be_bytes());
}

fn write_prop(
struct_block: &mut Vec<u8>,
strings_block: &mut Vec<u8>,
string_map: &mut BTreeMap<String, u32>,
prop: &DeviceTreeProperty,
) {
let name_offset = if let Some(offset) = string_map.get(prop.name()) {
*offset
} else {
let offset = u32::try_from(strings_block.len()).expect("string block length exceeds u32");
strings_block.extend_from_slice(prop.name().as_bytes());
strings_block.push(0);
string_map.insert(prop.name().to_owned(), offset);
offset
};

struct_block.extend_from_slice(&FDT_PROP.to_be_bytes());
struct_block.extend_from_slice(
&u32::try_from(prop.value().len())
.expect("property value length exceeds u32")
.to_be_bytes(),
);
struct_block.extend_from_slice(&name_offset.to_be_bytes());
struct_block.extend_from_slice(prop.value());
align(struct_block);
}

fn align(vec: &mut Vec<u8>) {
let len = vec.len();
let new_len = Fdt::align_tag_offset(len);
vec.resize(new_len, 0);
}
13 changes: 13 additions & 0 deletions tests/fdt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,16 @@ fn pretty_print() {
assert_eq!(s, expected, "Mismatch for {name}");
}
}

#[test]
#[cfg(feature = "write")]
fn round_trip() {
for (dtb, _dts, name) in ALL_DT_FILES {
use dtoolkit::model::DeviceTree;

let fdt = Fdt::new(dtb).unwrap();
let ir = DeviceTree::from_fdt(&fdt).unwrap();
let new_dtb = ir.to_dtb();
assert_eq!(dtb.to_vec(), new_dtb, "Mismatch for {name}");
}
}
30 changes: 30 additions & 0 deletions tests/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,33 @@ fn find_node_mut() {
// Find a non-existent node
assert!(tree.find_node_mut("/child-a/child-c").is_none());
}

#[test]
fn device_tree_format() {
let mut tree = DeviceTree::new();
tree.root.add_child(
DeviceTreeNode::builder("child-a")
.child(DeviceTreeNode::builder("child-a-a").build())
.build(),
);
tree.root
.add_child(DeviceTreeNode::builder("child-b").build());

let fds = tree.to_string();

assert_eq!(
fds,
r#"/dts-v1/;

/ {
child-a {
child-a-a {
};
};

child-b {
};
};
"#
);
}